How to return data to the calling activity in kotlin? - android

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

Related

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

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.

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();}

many activities with same content (adview, reward adview, functions...) how to avoid repeating code?

I have a gallery app, with some categories: animals, flowers... each gallery I created one activity, this activity in all galleries has the same content:
onCreate:
tSpeak = new TextToSpeech(this, new TextToSpeech.OnInitListener() { ...
AdView adView = new AdView(MainActivity.this); ...
//some checks to change images:
if(fase == 1) { imageView1.setImageResource(R.drawable.flower1); ...
if(fase == 100) ...
some setOnClickListener
after that some onRewardedVideo functions, a next() function that will intent to the same activity to show next image on this gallery (`fase` + 1)
What I'd like to know is, can I instead of repeating all this things (TextToSpeech, listeners, rewardedvideos functions, adview functions) just change the if checks in each activity gallery? this is the only thing that will change in each activity gallery, and I don't want to use just one activity for all categories because in this case I may have more than 2000 ifs and it is not good to work.
I'd like to just inject in each category the ifs to set imageresources there, without need to copy all activity again and again in each category. Any ideas?
You can create your common functionalities in a BaseActivity and extend your activities from this BaseActivity.
For the ifs conditional checking you can have a boolean field in your BaseActivity which you can override in your each subclass (In this case, your actual activities).
A better option could be having one activity only where your category is passed in the Bundle. Now based on your category, you can switch over the value and keeps your variations.
I suggest you use a single activity and pass a gallery ID as a parameter to your activity via an Intent extra.
You could get the image resource ids of the current gallery from a static map in your activity.
You could also use a ViewPager with one gallery per fragment, to enable your user to navigate quickly accross galleries.
Look at this for more info:
https://developer.android.com/training/animation/screen-slide

Equivalent of startActivityForResult() with Android Architecture Navigation

I have a workflow with 3 screens. From "screen 1" to access to "screen 2", the user must accept some kind of terms and conditions that I call in my picture "modal". But he only has to accept those conditions once. The next time he is on the first screen, he can go directly to screen 2. The user can chose to NOT accept the terms, and therefore we go back to "screen 1" and do not try to go to "screen 2".
I am wondering how to do it with the new navigation component.
Previously, what I would do it:
On screen 1, check if the user must accept the conditions
If no, start "screen 2" activity
If yes, use startActivityForResult() and wait result from the modal. Mark the terms as accepted. Start "screen 2"
But with the navigation graph, there is no way to start a Fragment to obtain a result.
I could mark the terms as accepted in the "modal" screen and start the "screen 2" from there. The thing is that to access to the screen 2, I need to do a network request. I do not want to duplicate the call to the API and processing its outcome in both "screen 1" and "modal".
Is there a way to go back from "modal" to "screen 1" with some information (user accepted the terms), using Jetpack navigation?
Edit: I currently get around it by using the same flow that Yahya suggests below: using an Activity just for the modal and using startActivityForResult from the "screen 1". I am just wondering if I could continue to use the navigation graph for the whole flow.
In the 1.3.0-alpha04 version of AndroidX Fragment library they introduced new APIs that allow passing data between Fragments.
Added support for passing results between two Fragments via new APIs on FragmentManager. This works for hierarchy fragments (parent/child), DialogFragments, and fragments in Navigation and ensures that results are only sent to your Fragment while it is at least STARTED. (b/149787344)
FragmentManager gained two new methods:
FragmentManager#setFragmentResult(String, Bundle) which you can treat similiarly to the existing Activity#setResult ;
FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener) which allows you to listen/observe result changes.
How to use it?
In FragmentA add FragmentResultListener to the FragmentManager in the onCreate method:
setFragmentResultListener("request_key") { requestKey: String, bundle: Bundle ->
val result = bundle.getString("your_data_key")
// do something with the result
}
In FragmentB add this code to return the result:
val result = Bundle().apply {
putString("your_data_key", "Hello!")
}
setFragmentResult("request_key", result)
Start FragmentB e.g.: by using:
findNavController().navigate(NavGraphDirections.yourActionToFragmentB())
To close/finish FragmentB call:
findNavController().navigateUp()
Now, your FragmentResultListener should be notified and you should receive your result.
(I'm using fragment-ktx to simplify the code above)
There are a couple of alternatives to the shared view model.
fun navigateBackWithResult(result: Bundle) as explained here https://medium.com/google-developer-experts/using-navigation-architecture-component-in-a-large-banking-app-ac84936a42c2
Create a callback.
ResultCallback.kt
interface ResultCallback : Serializable {
fun setResult(result: Result)
}
Pass this callback as an argument (note it has to implement Serializable and the interface needs to be declared in its own file.)
<argument android:name="callback"
app:argType="com.yourpackage.to.ResultCallback"/>
Make framgent A implement ResultCallback, fragment B by will get the arguments and pass the data back through them, args.callback.setResult(x)
It looks like there isn't equivalent for startActivityForResult in Navigation Component right now. But if you're using LiveData and ViewModel you may be interested in this article. Author is using activity scoped ViewModel and LiveData to achieve this for fragments.
Recently (in androidx-navigation-2.3.0-alpha02 ) Google was released a correct way for achieve this behaviour with fragments.
In short: (from release note)
If Fragment A needs a result from Fragment B..
A should get the savedStateHandle from the currentBackStackEntry, call getLiveData providing a key and observe the result.
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Type>("key")?.observe(
viewLifecycleOwner) {result ->
// Do something with the result.
}
B should get the savedStateHandle from the previousBackStackEntry, and set the result with the same key as the LiveData in A
findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result)
Related documentation
There is another alternative workaround. You can use another navigation action from your modal back to screen1, instead of using popBackStack(). On that action you can send whatever data you like to screen one. Use this strategy to make sure the modal screen isn't then kept in the navigation back stack: https://stackoverflow.com/a/54015319/4672107.
The only issue I see with this strategy is that pressing the back button will not send any data back, however most use cases require navigation after a specific user action and and in those situations, this workaround will work.
To get the caller fragment, use something like fragmentManager.putFragment(args, TargetFragment.EXTRA_CALLER, this), and then in the target fragment get the caller fragment using
if (args.containsKey(EXTRA_CALLER)) {
caller = fragmentManager?.getFragment(args, EXTRA_CALLER)
if (caller != null) {
if (caller is ResultCallback) {
this.callback = caller
}
}
}

Best way to start one activity or another basing on a variable

I want to develop an application that, at the beginning, checks a variable's value and, basing on this value, starts the activity A or the activity B, something like this:
protectec void onCreate(...){
boolean b = checkVariable();
if(b){
startActivityA();
} else {
startActivityb();
}
}
What I'm doing
This is the method I have currently implemented:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_limitation);
varChecker = new VarChecker(this);
if(varChecker.getRemainingUses() <= 0){
limitReached();
} else if(varChecker.isFirstTime()){
firstTime();
} else {
startMainActivity();
}
}
This is an activity that must be shown the first time the application is executed. Else, depending on the getRemainingUses() result, it must start the activity A or the activity B automatically.
The question
Is there any way to do what I want, without the need of create a new Activity, specially to avoid the super.onCreate(savedInstanceState) and the setContentView(R.layout.activity_limitation)?
Is there any way to do what I want, without the need of creating a new
Activity, specially to avoid the super.onCreate(savedInstanceState)
and the setContentView(R.layout.activity_limitation)?
I had a similar problem some time ago. As far as I know, It's not possible to avoid calling super.onCreate(savedInstance) and setContentView(R.layout.activity_limitation). It's design of the Android activity.
I see the following possible solutions:
if you want to choose an appropriate activity by e.g. clicking a
button, then you just need to create a new Intent basing on a
variable and there's no problem.
if you want to choose an activity in
a different moment of the flow - e.g. during the application start you can:
create a single activity for two options, use fragments and switch between them
create "dummy" activity with an empty layout, create an intent and switch to an appropriate activity basing on a variable and finish "dummy" activity. Please note that this solution is a kind of workaround and it's worth to spend some time to figure out something better, but I'm not sure if there is a recommended solution for such use cases.

Categories

Resources