Different behaviour when wrapping code in extension function - android

I'm starting my main activity from a callback, this works fine:
private val callback: BarcodeCallback = object : BarcodeCallback {
override fun barcodeResult(result: BarcodeResult) {
val intent = Intent(applicationContext, MainActivity::class.java)
startActivity(intent)
}
}
I found a generic extension method for launching activities:
inline fun <reified T : Any> Context.launchActivity(
options: Bundle? = null,
noinline init: Intent.() -> Unit = {}) {
val intent = newIntent<T>(this)
intent.init()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
startActivity(intent, options)
} else {
startActivity(intent)
}
}
inline fun <reified T : Any> newIntent(context: Context): Intent =
Intent(context, T::class.java)
When I use this method instead (applicationContext.launchActivity<MainActivity>()) I get an exception: Calling startActivity() from outside of an Activity context outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag
Why is the behaviour different? I'm not setting the flag in my original attempt, which works fine.

Related

Reload activity and call a function with the same button in Android Studio (Kotlin)

I want to create a button that allows me to both reload my activity and call a new function once the activity is reloaded. Unfortunately by calling two functions at the same time the second function I call after the activity refreshes does not work. How can I solve this problem which seems simple at first sight
fun newPie(valeur: Double){
config.addData(SimplePieInfo(valeur, Color.parseColor("#000000")))
config.drawText(false)
config.strokeMode(false)
anim.applyConfig(config)
anim.start()}
fun refresh() {
val intent = Intent(applicationContext, anychart::class.java)
startActivity(intent)
finish()
}
button.setOnClickListener(){
refresh()
newPie(valeur = 33.3)
}
If you want to call the new function once activity is reloaded, you should call that function into onCreate method of activity.
override fun onCreate(...) {
...
newPie(valeur = 33.3)
}
and button should only be used for refreshing the activity:
fun refresh() {
val intent = Intent(applicationContext, anychart::class.java)
startActivity(intent)
finish()
}
button.setOnClickListener(){
refresh()
}
As you want to restart the activity and hit the function then you should finish the activity, pass data to new instance of the activity so that you can check and trigger the function
Following code will help you
When you are finishing the activity just dont restart the activity but send some data with it as well.
var intent = Intent(this, anychart::class.java)
intent.putExtra("startFunction", true) // to trigger the function or not
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK // so any pending activity can be removed
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
Then in the onCreate function check the data and trigger function when the true is sent
intent?.extras?.let {
if(it.containsKey("startFunction")){
val isStart = it.getBoolean("Data", false)
if(isStart){
newPie(valeur = 33.3)
}
}
}
The extra function call needs to happen in the newly created Activity instance, and it has to be done after onCreate() has been called. You could directly put this function call in onCreate(), but if you don't want it to be called the very first time the Activity is opened, then you need to add an extra to your Intent and use that extra to determine if the function should be called. Like this:
companion object {
private const val IS_REFRESH_KEY = "is_refresh"
}
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
// ...
button.setOnClickListener(){
refresh()
}
val isRefresh = intent.extras?.getInt(IS_REFRESH_KEY) == 1
if (isRefresh) {
newPie(valeur = 33.3)
}
}
fun refresh() {
val intent = Intent(applicationContext, anychart::class.java).apply {
putExtra(IS_REFRESH_KEY, 1)
}
startActivity(intent)
finish()
}
If you want the 33.3 to be customizable, you could use a Float extra instead:
companion object {
private const val REFRESH_PIE_VALUE = "refresh_pie_value"
}
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
// ...
button.setOnClickListener(){
refresh()
}
val refreshPieValue = intent.extras?.getFloat(REFRESH_PIE_VALUE) ?: -1f
if (refreshPieValue >= 0f) {
newPie(refreshPieValue)
}
}
fun refresh() {
val intent = Intent(applicationContext, anychart::class.java).apply {
putExtra(REFRESH_PIE_VALUE, 33.3) // customize the value here
}
startActivity(intent)
finish()
}

How to get result using registerForActivityResult from within ktor's Routing call running in a non-activity class?

How to get result from another activity (registerForActivity) from with in ktor's Routing API call (eg. /POST) running in a non-activity class?
Background: For an Android app, I run ktor server engine 'netty' in a non-activity class HttpServer.kt. I need to call another app's activity from with in ktor's Routing' POST handler, so I pass 'appCompatActivity' from MainActivity.kt. That's done, just because, I assume, registerForActivityResult() has dependency on UI/life cycle class.
Problem arises when running this as below, as registerForActivityResult() requires to be run earlier (like onCreate() ?), and I don't have such a class in this non-activity class. Moreover, the callback to run when ActivityResult is returned needs to call ktor ApplicationCall's respond which is also a suspend function.
class HttpServer(
private val applicationContext: AppCompatActivity
) {
private val logger = LoggerFactory.getLogger(HttpServer::class.java.simpleName)
private val server = createServer()
private fun ApplicationCall.startSaleActivityForResult() { // <====== *
val activityLauncherCustom =
applicationContext.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK || result.resultCode == Activity.RESULT_CANCELED) {
val transactionResultReturned = result.data
// Handle the returned result properly using transactionResultReturned
GlobalScope.launch {
respond(status = HttpStatusCode.OK, TransactionResponse())
}
}
}
val intent = Intent()
// Ignoring statements to create proper action/data intent
activityLauncherCustom.launch(intent) // <====== *
}
fun start() = server.start()
fun stop() = server.stop(0, 0)
private fun createServer(): NettyApplicationEngine {
return GlobalScope.embeddedServer(Netty) {
install(CallLogging)
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
routing {
route("/") {
post {
call.startSaleActivityForResult() // <====== *
}
}
}
}
}
private fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
CoroutineScope.embeddedServer(
factory: ApplicationEngineFactory<TEngine, TConfiguration>,
module: Application.() -> Unit
): TEngine {
val environment = applicationEngineEnvironment {
this.parentCoroutineContext = coroutineContext + parentCoroutineContext
this.log = logger
this.module(module)
connector {
this.port = 8081
}
}
return embeddedServer(factory, environment)
}
}
Above is what I tried, but gives below error. And I don't have onCreate on this non-activity class.
java.lang.IllegalStateException: LifecycleOwner com.youtap.upti.MainActivity#38dcf06 is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.
Any suggestions to resolve this problem would be grateful.
Below same above snippet as a screenshot to display helper text on declaration/param types from Android Studio:
And I invoke this server class from onCreate() of MainActivity:
To solve your problem and to hide the complexity you can create an intermediate class for launching activity and waiting for a result to come:
import kotlinx.coroutines.channels.Channel
class Repository(private val activity: MainActivity) {
private val channel = Channel<Int>(1)
suspend fun get(input: String): Int {
activity.activityLauncher.launch(input)
return channel.receive()
}
suspend fun callback(result: Int) {
channel.send(result)
}
}
You can store a reference to a repository and an activity launcher in the MainActivity class:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
HttpServer(this#MainActivity).also { it.start() }
}
}
val activityLauncher = registerForActivityResult(MySecondActivityContract()) { result ->
GlobalScope.launch {
repository.callback(result!!)
}
}
val repository = Repository(this)
}
My second activity and a contract looks like the following:
class ChildActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_child)
val result = Intent()
result.putExtra("name", 6666)
result.data = Uri.parse("http://mydata")
setResult(Activity.RESULT_OK, result)
finish()
}
}
class MySecondActivityContract : ActivityResultContract<String, Int?>() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(context, ChildActivity::class.java)
.putExtra("my_input_key", input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Int? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getIntExtra("name", 42)
}
override fun getSynchronousResult(context: Context, input: String?): SynchronousResult<Int?>? {
return if (input.isNullOrEmpty()) SynchronousResult(42) else null
}
}
The most simplest part is routing handler:
routing {
route("/") {
post {
val result = (applicationContext as MainActivity).repository.get("input")
call.respondText { result.toString() }
}
}
}
This solution works but only one request is processed at the same time and it's not robust because Activity may be destroyed before HTTP server or repository objects.

Kotlin wait for async with coroutine

I would like to open a new activity when phoneViewModel and ScanViewModel are instantiated. They are instantiated by calling an async function InitialRead(). I'm logging each step, atm they are logged as done3 => done2 => done1
I would like to have them in this order:
done1 => done2 => done3
I have following code:
class MainBusinessActivity : AppCompatActivity() {
private lateinit var scanViewModel: ScanViewModel
private lateinit var phoneViewModel: PhoneViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_business)
}
private fun startEntitySetListActivity() = GlobalScope.async {
val sapServiceManager = (application as SAPWizardApplication).sapServiceManager
sapServiceManager?.openODataStore {
phoneViewModel = ViewModelProvider(this#MainBusinessActivity).get(PhoneViewModel::class.java).also {it.initialRead{Log.e("done", "done1")}}
scanViewModel = ViewModelProvider(this#MainBusinessActivity).get(ScanViewModel::class.java).also {it.initialRead{Log.e("done", "done2")}}
}
}
override fun onResume() {
super.onResume()
//startEntitySetListActivity()
runBlocking {
startEntitySetListActivity().await()
val intent = Intent(this#MainBusinessActivity, HomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Log.e("done", "done3")
startActivity(intent)
}
}
}
What am I doing wrong? Can someone correct my code?
Never use runBlocking in an Android app. runBlocking completely defeats the purpose of using coroutines, and can lead to an ANR. You also probably should never use GlobalScope, which leads to UI leaks. You might possibly need it for some kind of long-running task that doesn't make sense to put in a service but doesn't have dependency on any UI components, but I can't think of any examples
You also shouldn't be instantiating your ViewModels in the background. That should be done in onCreate().
Make this function a suspend function, and it can break down the two tasks in the background simultaneously before returning.
Start your coroutine with lifecycleScope.
Assuming sapServiceManager?.openODataStore is an asynchronous task that takes a callback, you will need to wrap it in suspendCoroutine.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_business)
phoneViewModel = ViewModelProvider(this#MainBusinessActivity).get(PhoneViewModel::class.java)
scanViewModel = ViewModelProvider(this#MainBusinessActivity).get(ScanViewModel::class.java)
}
private suspend fun startEntitySetListActivity() = coroutineScope {
val sapServiceManager = (application as SAPWizardApplication).sapServiceManager
sapServiceManager ?: return
suspendCoroutine<Unit> { continuation ->
sapServiceManager.openODataStore { continuation.resume(Unit) }
}
listOf(
launch {
phoneViewModel.initialRead{Log.e("done", "done1")}
},
launch {
scanViewModel.initialRead{Log.e("done", "done2")}
}
).joinAll()
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
startEntitySetListActivity()
val intent = Intent(this#MainBusinessActivity, HomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Log.e("done", "done3")
startActivity(intent)
}
}

Activity Results API returned data is null

I am trying out the new Activity Results API by trying to return a parcelable dataesque class from a child activity. Using Alpha4 of the library.
I have setup the Intent with a custom contract 'AddAttendeeContract' as per my understanding of the docs. It compiles and runs and as far as I can see the correct methods are being called but the data is just null.
What might I be missing?
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
... //boilerplate setup nonsense
fab.setOnClickListener {
addAttendee()
}
}
private val addAttendee = registerForActivityResult(AddAttendeeContract()) { attendee: AttendeeData? ->
println("Attendee") // this does not print out
println(attendee) // this does not either
}
}
And the contract
class AddAttendeeContract : ActivityResultContract<Void?, AttendeeData?>() {
override fun createIntent(
context: Context,
input: Void?
): Intent =
Intent(context, AddAttendeeActivity::class.java)
override fun parseResult(
resultCode: Int,
intent: Intent?
): AttendeeData? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getParcelableExtra<AttendeeData>("attendee")
}
}
Finally is the invocation in the child activity class.
class AddAttendeeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
... //boilerplate
add.setOnClickListener { //button on a form
val name: String = view.name.text.toString().trim()
val rate: Double = view.rate.text.toString().trim().toDouble()
val number: Int = view.number.text.toString().trim().toInt()
val intent = Intent(this, MainActivity::class.java).apply {
putExtra("attendee", AttendeeData(name=name, rate=rate, number=number))
}
setResult(Activity.RESULT_OK, intent)
startActivity(intent)
}
}
}
Any insights as to what is going on?
This is solved. The problem was that the second activity was startign a new intent, rather than finishing and returning to the old one.
In the second/child activity had to change the line:
startActivity(intent)
to
finish() and things all worked as expected.

Android - How to pass a function to an Activity?

I am attempting to create a Builder for an activity. The reason is because this activity can be started many different ways. I created a Builder class like this:
class ActivityBuilder {
private var showToolBar = false
private var postExecutable: (() -> Unit)? = null
fun showToolbar(boolean: Boolean) : ActivityBuilder {
this.showToolBar = boolean
return this
}
fun setPostExecutable(function: () -> Unit) : ActivityBuilder {
this.postExecute = function
return this
}
fun start(context: Context){
val intent = Intent(context, Activity::class.java)
context.startActivity(intent)
}
}
The idea is to call something like this and have access to these fields inside of the activity.
ActivityBuilder().showToolbar(false).setPostExecutable { { doSomething() } }.start(this)
I guess I could also use a companion object and that would serve the same purpose.
companion object Builder {
private var showToolBar = false
private var postExecute: (() -> Unit)? = null
fun showToolbar(boolean: Boolean) : Builder {
this.showToolBar = boolean
return this
}
fun setPostExecutable(function: () -> Unit) : Builder {
this.postExecute = function
return this
}
fun start(context: Context){
val intent = Intent(context, AuthActivity::class.java)
context.startActivity(intent)
}
}
The issue is coming mostly from the "postExecutable" field. I need to call the function at a certain point but it is not parcelable, so I cannot pass it through the intent when starting activity.
If anyone has a solution, I appreciate it!
This is one solution I found, may not be the most elegant. I created a broadcast receiver that I start at the same time as my activity using the parent context.
class ActivityBuilder(private val context: Context) {
private var postSuccessExecutable: (() -> Unit)? = null
...
private fun setupReceiver(){
val filter = IntentFilter()
filter.addAction("SUCCESS")
val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, intent: Intent?) {
context.unregisterReceiver(this)
if (intent?.action == "SUCCESS"){
Toast.makeText(context, "Successful", Toast.LENGTH_SHORT).show()
postSuccessExecutable?.invoke()
}
}
}
context.registerReceiver(authReceiver, filter)
}
...
}
When I want to trigger the function, I just send a broadcast:
private fun sendSuccessBroadcast(data: String){
val intent = Intent()
intent.action = "SUCCESS"
intent.putExtra("data", String)
requireContext().sendBroadcast(intent)
}

Categories

Resources