How to navigate to deeply nested screen composable from the Activity's onCreate in Jetpack Compose - android

Suppose I have a notification, that when clicked, launches my app's activity. It's a notification about a message, in a conversation, and so it launches the activity passing the conversationId as an argument. When the activity is launched by that intent from the notification, it should open MessagesScreen, which is a deeply nested screen in the app, passing to it conversationId.
What is the best way to do this in Compose? In the good old Fragments or Activities you just navigated straight to it, but with Compose is a little trickier. The path to the MessagesScreen is as follows:
SplashScreen (checks for authentication) -> HomeScreen (if authenticated) -> ConversationScreen -> MessagesScreen
I can't just navigate straight to MessagesScreen by having the compose's NavController be stored in the Activity, since I need to go through SplashScreen to check for authentication. Also, I don't know the Compose's implication of navigating to a deeply nested component from the Activity's onCreate().
What I currently do is have a field in my global ViewModel called notificationConversationId, that is set on my Activity's onCreate if it was passed by the notification's intent:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val conversationId = intent.getStringExtra("conversation_id") ?: ""
globalViewModel.notificationConversationId = conversationId
// ...
Then, in a LaunchedEffect in my HomeScreen I observe this field, and if it is not empty, I navigate to the MessagesScreen, and set it to an empty string, so the LaunchedEffect is not executed again.
val conversationId = globalViewModel.notificationConversationid
LaunchedEffect(conversationId) {
if (conversationId.isNotEmpty()) {
val path = getMessagespath(conversationId = conversationId)
globalViewModel.notificationConversationId = ""
navController.navigate(path)
}
}
It works, but it is horrendous. Is there a better way to accomplish this in Compose? Thanks in advance.

Related

How do I Restore Navigation State on Startup (Jetpack Compose)?

When I start up my app I want it to be able to restore the user's last session if it wasn't completed. How do I restore the navigation backstack while using Jetpack Compose?
My application presents educational sessions, with pages being generated dynamically. Back/up navigation works fine.
#Composable
fun SessionScreenLayout(
pageIndex: Int,
mainViewModel: MainViewModel,
) {
...
}
If a session is not completed when the app shuts down then I manually store that session and I want to be able to restore it when the user starts up again. Importantly I want back navigation to navigate sequentially backwards through my Session pages, after I have restored a session
I've tried populating the backstack but creating a new NavBackStackEntry seems to have 2 options. First doesn't work because the new NavBackStackEntry has the same id as the currentBackStackEntry. After populating the back stack I navigate to the last page and that works fine. but if I navigate backwards the page doesn't recompose properly:
val entry = NavBackStackEntry(
entry = currentBackStackEntry!!,
arguments = Bundle().apply {
...
}
)
navController.backQueue.addLast(entry)
navController.navigate(Screen.SessionScreen.withIndex(userSession.highestPageIndexViewed))
Else have tried using NavBackStackEntry.create():
val entry = NavBackStackEntry.create(
hostLifecycleState = ?,
viewModelStoreProvider = ?,
...
)
navController.backQueue.addLast(entry)
navController.navigate(Screen.SessionScreen.withIndex(userSession.highestPageIndexViewed))
With this I need the "viewModel" and "lifecycleOwner" private fields values from the navController for the constructor AFAIK. Without those I'm getting an exception. Again the last navigation works, but when I go back:
java.lang.IllegalStateException: You must call setViewModelStore() on your NavHostController before accessing the ViewModelStore of a navigation graph.
How do I get this working?

How to destroy a ViewModel when user leave a screen

In my project I have a splash screen, when it is displayed, my app loading some startup data from server, after loading the data shows another screen.
For splash screen I create a ViewModel, but it stays in memory all the time. How to destroy it correctly?
Thank you for help!
#HiltViewModel
class SplashViewModel #Inject constructor (private val repository: Repository) {
....
}
#Composable
fun SplashScreen(vm: SplashViewModel) {
...
}
#Composable
fun Navigate() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "splash") {
composable("splash") {
SplashScreen(vm = hiltViewModel())
}
composable("main") {
MainScreen(...) // When shows MainScreen, SplashViewModel object still is in memory
}
}
}
Your viewmodel stays in memory because your splash screen is your root destination, and as such it stays always on the stack as the bottom entry.
If you want your splash viewmodel to be automatically destroyed when you leave your splash screen you should pop it from the backstack when you navigate to your main screen, using popUpTo.
Another option you could consider is to make your main screen the root destination and then navigate from that screen to splash if you are starting the app fresh.
Using hiltViewModel and scoping the viewmodel to the nav graph destination as you do will ensure the viewmodel is destroyed when the user leaves that screen, provided it's not in the backstack.
It is not explicitly supported in Android as far as I know. However, you could create a method named onViewModelCleared() inside the viewmodel itself and pass null to all the nullable objects, and something lightweight to non-null objects.

How can I use button in one activity to do something in second activity?

I got a question. I have two activities in my app. In first one, when I click the button, I need something to happen in the second one. How can I do it? If that button would be in the second activity I would just do it by:
button.setOnClickListener {}
But how can I do it when button is in the other activity? It's worth adding that code, that tells what should happen, must be in that second activity, just like it was in that "setOnClickListener". Sorry, I'm starting with Android development.
You could communicate between two activities via broadcast or intent.
But it make logic more complex.
So I suggest use two fragments instead two activities.
If you use two fragment in one activity, you can easy communicate between two fragments.
You can look more detailed information about fragment from this URL.
https://developer.android.com/guide/fragments
To achieve the intended flow you may try the below approach,
Start activity 2 on button click from activity 1.
On activity 2 place your code in onCreate so, once activity 2 loads up your code will fire up.
Activity 1:
button.setOnClickListener{
val intent = Intent(this, second::class.java)
startActivity(intent)
}
Activity 2:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CallDefinedFunctionHere();
}
You cannot be sure that both activities are present at the same moment since the system might destroy inactive one therefore you cannot trigger any code from activity A inside activity B.
What you can do you can start activity B with an intent and some parameters describing what should happen inside activity B.
or
You can communicate by writing something down to a persistent storage (like SharedPreferences) and then when the other activity is resumed (active again) reading it, reacting to it and then removing it from the storage (to make sure you do not handle it twice).
You can pass data in the intent that opens the second activity.
// In first activity:
buttonX.setOnClickListner {
val intent = Intent(MySecondActivity::class.java).apply {
putExtra("wasFromButtonX", true)
}
startActivity(intent)
}
// In second activity:
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val wasLaunchedFromButtonX = intent.getBooleanExtra("wasFromButtonX", false)
// Above line uses false as default, so it will only be true if you explicitly
// put the extra in the Intent that started this Activity.
if (wasLaunchedFromButtonX) {
// do alternate setup here
}
}
How to pass information between Activities is explained in the introductory documentation here.
Create a function in class where 2nd activity are defined like this.
public void refresh(){}
Now Call that in your 1st activity where you want to call 1st after any action.
button.setOnClickListener {((MainActivity) Objects.requireNonNull(getActivity())).refresh();}

How to return data to the calling activity in kotlin?

I am getting confused on how to return data back to the previous activity. Perhaps because of previous experience with old styles like windows forms in dot-net.
Scenario: my simple android app starts with the MainActivity, showing some values in some units, e.g. 'you are 1.86 m tall', and having a tools icon in the menu bar. Clicking this icon starts the ToolsActivity where the user may select some settings, like wether she prefers american or metric units, e.g. meter versus foot. When done, the user clicks the back arrow in the top bar to return to the MainActivity, that should show the values in the selected units.
This is how I store the current setting of the units system:
const val EXTRA_UNITSYSTEM = ".UNITSYSTEM"
class MainActivity : AppCompatActivity() {
var TheUnitSystem : String = "metric"
I found out how to use putExtra() in the intent to startActivity() in the MainActivity to start the ToolsActivity,:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.tools -> {
val intent = Intent(this, ToolsActivity::class.java)
intent.putExtra(EXTRA_UNITSYSTEM, TheUnitSystem)
startActivity(intent)
and getStringExtra() to get the current value of TheUnitSystem in the ToolsActivity so the radiobutton can be initialized to the current setting:
class ToolsActivity : AppCompatActivity() {
var TheUnitSystem: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
...
this.TheUnitSystem = intent.getStringExtra(EXTRA_UNITSYSTEM)!!
This actually works, but leaves the problem of how to return the data, which is possibly changed to point at another units system, back to the MainActivity when the ToolsActivity ends (or stops, or finishes, or gets destroyed...)
Initially, my guess was that intents are used to go to another activity, and that it needs another intent to go back to the previous activity.
Another guess was that you use EXTRA data on the intent to get data to ToolsActivity, so somehow the EXTRA data is also to be used to get data back to the MainActivity.
Both guesses seem to be naive.
Then I found out about starting the ToolsActivity with startActivityForResults(), as this is designed to get a result back from the second activity. However, the stories that I kind of grasp are in java, and the stories from developer.android.com that use kotlin are much more abstract and seem to describe yet different methods.
Can someone point me at a basic kotlin example for returning a simple String back to MainActivity, preferably using startActivityForResults() ?
From programming in simple Windows apps, I would guess that it would be even simpler to use application global (static) data. If that were possible, we should not need EXTRA stuff and special ForResult() methods, so perhaps this is also a dead end street?
extra info:
What may make my simple project a bit special is that I don't have a button in the layout of the second activity for going back to the MainActivity. The second activity is started from clicking a MenuItem on the Toolbar widget, defined in the res/menu/menu_main.xml layout. The second activity is shown with a back arrow in the top bar which is not in the layout of the second activity. Advantage is that it is really a Settings screen. Disadvantage is that it is not a plain normal activity where you put a back button in the layout like in most coding examples.
You are on the right track, thinking about using startActivityForResults and I'm guessing all that is missing is connecting the original activity to receive results.
In the original activity you need to override onActivityResult like this:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// If you have multiple activities returning results then you should include unique request codes for each
if (requestCode == <pick a code>) {
// The result code from the activity started using startActivityForResults
if (resultCode == Activity.RESULT_OK) {
}
}
}
Then, in the activity you want to return information from, do something like this:
val intent = Intent()
intent.putExtra("ActivityResult", "<Data to return>")
setResult(RESULT_OK, intent)
finish()
This is one example, but they are all similar: Example

Why Android Navigation Component screen not go back to previous Fragment,but a method in onViewCreated of previos Fragment being called?

I have 2 fragment call CreateRoomFragment and DisplayPhotoFragment,the navigation graph is look like this:
<navigation>
<fragment
android:id="#+id/createRoomFragment"
android:name="package.room.CreateRoomFragment"
android:label="Create a room"
tools:layout="#layout/fragment_create_room">
<action
android:id="#+id/action_createRoomFragment_to_roomFragment"
app:destination="#id/roomFragment" />
<action
android:id="#+id/action_createRoomFragment_to_displayPhotoFragment"
app:destination="#id/displayPhotoFragment" />
</fragment>
<fragment
android:id="#+id/displayPhotoFragment"
android:name="package.fragment.DisplayPhotoFragment"
android:label="fragment_display_photo"
tools:layout="#layout/fragment_display_photo" >
<argument android:name="bitmap"
app:argType="android.graphics.Bitmap"/>
</fragment>
So when I wanna to move from CreateRoomFragment to DisplayPhotoFragment,I use the do as below:
NavDirections action = CreateRoomFragmentDirections.actionCreateRoomFragmentToDisplayPhotoFragment(selectedPhoto);
Navigation.findNavController(view).navigate(action);
Doing this,I can navigate to DisplayPhotoFragment.
But when I press back button of the device and also the Back arrow from the toolbar,it cant go back to CreateRoomFragment.
I tried this,but still unable to back to previous fragment:
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
navController.navigateUp(); //I tried this
navController.popBackStack(R.id.createRoomFragment,false); //and also this
}
});
Main Problem now:
By using the code above,the screen didnt go back to previous Fragment(CreateRoomFragment).It still stuck in DisplayPhotoFragment,but at the same time,an API method in CreateRoomFragment onViewCreated section is being called.
What causing this? and how can I solve this problem?
I had the same problem. For me the issue was that I was using a LiveData boolean to decide when to go to the next fragment. When I then navigated back/up the boolean was still true so it would automatically navigate forward again.
Android maintains a back stack that contains the destinations you've visited. The first destination of your app is placed on the stack when the user opens the app. Each call to the navigate() method puts another destination on top of the stack. Tapping Up or Back calls the NavController.navigateUp() and NavController.popBackStack() methods, respectively, to remove (or pop) the top destination off of the stack.
NavController.popBackStack() returns a boolean indicating whether it successfully popped back to another destination. The most common case when this returns false is when you manually pop the start destination of your graph.
When the method returns false, NavController.getCurrentDestination() returns null. You are responsible for either navigating to a new destination or handling the pop by calling finish() on your Activity.
When navigating using an action, you can optionally pop additional destinations off of the back stack by using popUpTo and popUpToInclusive parameter of the action.
class MyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (true == conditionForCustomAction) {
CustomActionHere()
} else NavHostFragment.findNavController(this#MyFragment).navigateUp();
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, onBackPressedCallback
)
...
}
The best solution for handling navigation using live data is to use the SingleLiveEvent.
You can always use this class which is an extension of MutableLiveData.
https://gist.githubusercontent.com/cdmunoz/ebe5c4104dadc2a461f512ea1ca71495/raw/a17f76754f86a4c0b1a6b43f5c6e6d179535e627/SingleLiveEvent.kt
For a detail run down of this check:
https://proandroiddev.com/singleliveevent-to-help-you-work-with-livedata-and-events-5ac519989c70
Had a similar issue. We still have multiple activities with nav component.
So imagine activity A -> activity B, activity B has its own nav and fragments. When the initial fragment tries to pop the back stack there is nowhere to pop back to and the nav controller does not know to finish the activity. So one solution I found was to do
if (!findNavController().popBackStack()) activity?.finish()
If nav controller can not pop back it will finish activity.
You can use MutableSharedFlow instead on MutableLiveData if you want to observe the Event only once.
in your viewModel:
private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow() // read-only public view
suspend fun postEvent() {
_events.emit(event) // suspends until subscribers receive it
}
In your Activity/Fragment class:
lifecycleScope.launchWhenStarted {
viewModel.events.collect {
}
}
viewModel.postEvent()
This will prevent observing data continuously when going back to fragment.

Categories

Resources