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)
}
Related
I have screens.
StoryScreen (from Flutter)
MainActivity (from Android Activity, extend to FlutterActivity)
MainUnityActivity (from Android Activity, extend to AppCompatActivity)
The actually screen is only two, StoryScreen and MainUnityActivity. MainActivity only for host.
In MainActivity we define intent and method channel.
class MainActivity : FlutterActivity() {
private val tag = "MAIN"
private val channel = "NATIVE_EXPERIMENT"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor, channel).setMethodCallHandler { call, result ->
if (call.method.equals("goToUnityActivity")) {
val arg = call.arguments as Map<String, Any>
val gameType = arg.getValue("gameType") as String
val catalogURL = arg.getValue("catalogURL") as String
goToUnityActivity(gameType, catalogURL)
result.success(null)
} else {
result.notImplemented()
}
}
}
private fun goToUnityActivity(gameType: String, catalogURL: String) {
val intent = Intent(this, MainUnityActivity::class.java)
intent.putExtra("gameType", gameType)
intent.putExtra("catalogURL", catalogURL)
startActivityForResult(intent, MainUnityActivity.REQUEST_CODE_FROM_UNITY)
}
private fun notifyFlutterBackFromUnity(data: String?) {
MethodChannel(flutterEngine?.dartExecutor, channel).invokeMethod("backFromUnity", data)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == MainUnityActivity.REQUEST_CODE_FROM_UNITY) {
if (resultCode == MainUnityActivity.RESULT_CODE_BACK_FROM_UNITY) {
val listOfReport = data!!.getStringExtra("listOfReport")
Log.i(tag, "/// [MainActivity] back from unity --> $listOfReport")
notifyFlutterBackFromUnity(listOfReport)
}
}
}
}
If I exit from MainUnityActivity, I can send data from Android to Flutter side, but how if we still in MainUnityActivity but want to send data from Android to Flutter side?
class MainUnityActivity : UnityPlayerActivity() {
private val tag = "MIDDLEWARE_UNITY"
private val listOfVehicleNames: MutableList<String> = mutableListOf()
companion object {
const val RESULT_CODE_BACK_FROM_UNITY = 110
const val REQUEST_CODE_FROM_UNITY = 1
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
listOfVehicleNames.clear()
}
private fun backFromUnity() {
Log.i(tag, "/// [MainUnityActivity] BackButtonClick")
val resultIntent = Intent().apply {
putExtra("listOfReport", listOfVehicleNames.toString())
}
setResult(RESULT_CODE_BACK_FROM_UNITY, resultIntent)
finish()
}
override fun BackButtonClick() {
Log.i(tag, "/// [MainUnityActivity] BackButtonClick")
backFromUnity()
}
override fun ReportButtonClick() {
Log.i(tag, "/// [MainUnityActivity] ReportButtonClick")
showDialog()
}
private fun notifyFlutterReportFromUnity() {
val json = """{"title": "JSON Title", "notes": "JSON Notes"}"""
Log.i(tag, "/// [MainUnityActivity] YesReportClick :$json")
listOfVehicleNames.add(json)
// TODO: I want send data to Flutter without close this activity
}
private fun showDialog() {
val builder = AlertDialog.Builder(this)
builder.setTitle("Report")
builder.setMessage("Is there something wrong ?")
builder.setPositiveButton(
"Yes"
) { _, _ ->
Toast.makeText(this, "Okay, we're sorry", Toast.LENGTH_SHORT).show()
notifyFlutterReportFromUnity()
}
builder.setNegativeButton(
"No"
) { _, _ ->
// User click no
}
builder.setNeutralButton("Cancel") { _, _ ->
// User cancelled the dialog
}
builder.show()
}
}
Finally, I fix it using MethodChannel, EventChannel, and BroadcastReceiver.
Method channel: A named channel for communicating with platform plugins using asynchronous method calls.
Event Channel: A named channel for communicating with platform plugins using event streams.
The example project is here: https://github.com/rrifafauzikomara/example_flutter_method_channel
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.
I'm writing tests to verify the reception of the transmitting receivers but for some reason, the receiver is never registered or the intent is never sent.
I guess there should be a problem with the Context but, no luck yet finding it.
This is the BroadcastFactory.kt:
object BroadcastFactory {
private lateinit var intent: Intent
fun build(
action: String,
flag: Int? = null,
): BroadcastFactory {
intent = Intent().apply {
this.action = action
this.flags = flag ?: 0
}
return this
}
fun send(
context: Context
): Intent {
context.sendBroadcast(intent)
return intent
}
}
And this is the test file BroadcastTest.kt:
#RunWith(AndroidJUnit4::class)
#SmallTest
class BroadcastTest {
lateinit var intents: MutableList<Intent>
lateinit var latch: CountDownLatch
private lateinit var receiver: BroadcastReceiverTester
inner class BroadcastReceiverTester : BroadcastReceiver() {
override fun onReceive(p0: Context?, intent: Intent?) {
intent?.let {
intents.add(it)
latch.countDown()
}
}
}
private val context: Context = getInstrumentation().targetContext
#Before
fun setUp() {
intents = mutableListOf()
latch = CountDownLatch(1)
receiver = BroadcastReceiverTester()
LocalBroadcastManager.getInstance(context).registerReceiver(
receiver,
IntentFilter.create(
Constants.ACTION, "text/plain"
)
)
}
#Test
fun testBroadcastReception() {
BroadcastFactory
.build(Constants.ACTION, Constants.FLAG)
.send(context)
// assert broadcast reception (NOT WORKING)
latch.await(10, TimeUnit.SECONDS)
assertThat(intents.size).isEqualTo(1)
}
#After
fun tearDown() {
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver)
}
}
I'm using a CountDownLatch to wait 10 seconds for the receiver, plus, its value can be asserted. Besides, I set a list of Intents to check the number of registrations/receptions.
There is something I'm missing here? Different context provider? Robolectric runner?
Thanks
Is solved it by changing the receiver with this:
context.registerReceiver(
receiver,
IntentFilter(
Constants.ACTION
)
)
Thanks to #selvin and #mike-m for the help!
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.
I have an activity to perform rest API everytime it opened and i use MVVM pattern for this project. But with this snippet code i failed to get updated everytime i open activity. So i debug all my parameters in every line, they all fine the suspect problem might when apiService.readNewsAsync(param1,param2) execute, my postValue did not update my resulRead parameter. There were no crash here, but i got result which not updated from result (postValue). Can someone explain to me why this happened?
Here what activity looks like
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityReadBinding>(this,
R.layout.activity_read).apply {
this.viewModel = readViewModel
this.lifecycleOwner = this#ReadActivity
}
readViewModel.observerRead.observe(this, Observer {
val sukses = it.isSuccess
when{
sukses -> {
val data = it.data as Read
val article = data.article
//Log.d("-->", "${article.toString()}")
}
else -> {
toast("ada error ${it.msg}")
Timber.d("ERROR : ${it.msg}")
}
}
})
readViewModel.getReadNews()
}
Viewmodel
var observerRead = MutableLiveData<AppResponse>()
init {
observerRead = readRepository.observerReadNews()
}
fun getReadNews() {
// kanal and guid i fetch from intent and these value are valid
loadingVisibility = View.VISIBLE
val ok = readRepository.getReadNews(kanal!!, guid!!)
if(ok){
loadingVisibility = View.GONE
}
}
REPOSITORY
class ReadRepositoryImpl private constructor(private val newsdataDao: NewsdataDao) : ReadRepository{
override fun observerReadNews(): MutableLiveData<AppResponse> {
return newsdataDao.resultRead
}
override fun getReadNews(channel: String, guid: Int) = newsdataDao.readNews(channel, guid)
companion object{
#Volatile private var instance: ReadRepositoryImpl? = null
fun getInstance(newsdataDao: NewsdataDao) = instance ?: synchronized(this){
instance ?: ReadRepositoryImpl(newsdataDao).also {
instance = it
}
}
}
}
MODEL / DATA SOURCE
class NewsdataDao {
private val apiService = ApiClient.getClient().create(ApiService::class.java)
var resultRead = MutableLiveData<AppResponse>()
fun readNews(channel: String, guid: Int): Boolean{
GlobalScope.launch {
val response = apiService.readNewsAsync(Constants.API_TOKEN, channel, guid.toString()).await()
when{
response.isSuccessful -> {
val res = response.body()
val appRes = AppResponse(true, "ok", res!!)
resultRead.postValue(appRes)
}
else -> {
val appRes = AppResponse(false, "Error: ${response.message()}", null)
resultRead.postValue(appRes)
}
}
}
return true
}
}
Perhaps this activity is not getting stopped.
Check this out:
When you call readViewModel.getReadNews() in onCreate() your activity is created once, only if onStop is called will it be created again.