Integrating Jitsi Sample in my project. but meeting unable to establish getting Cannot join, view is null error in logs. can anyone suggest what could be the issue?
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
onBroadcastReceived(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val serverURL: URL = try {
URL("https://meet.jit.si")
} catch (e: MalformedURLException) {
e.printStackTrace()
throw RuntimeException("Invalid server URL!")
}
val defaultOptions = JitsiMeetConferenceOptions.Builder()
.setServerURL(serverURL)
// When using JaaS, set the obtained JWT here
//.setToken("MyJWT")
// Different features flags can be set
//.setFeatureFlag("toolbox.enabled", false)
//.setFeatureFlag("filmstrip.enabled", false)
.setFeatureFlag("welcomepage.enabled", false)
.build()
JitsiMeet.setDefaultConferenceOptions(defaultOptions)
registerForBroadcastMessages()
binding.button4.setOnClickListener{
onButtonClick()
}
}
override fun onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
super.onDestroy()
}
private fun onButtonClick() {
val text = binding.conferenceName.text.toString()
if (text.isNotEmpty()) {
// Build options object for joining the conference. The SDK will merge the default
// one we set earlier and this one when joining.
val options = JitsiMeetConferenceOptions.Builder()
.setRoom(text)
// Settings for audio and video
//.setAudioMuted(true)
//.setVideoMuted(true)
.build()
// Launch the new activity with the given options. The launch() method takes care
// of creating the required Intent and passing the options.
JitsiMeetActivity.launch(this, options)
}
}
private fun registerForBroadcastMessages() {
val intentFilter = IntentFilter()
/* This registers for every possible event sent from JitsiMeetSDK
If only some of the events are needed, the for loop can be replaced
with individual statements:
ex: intentFilter.addAction(BroadcastEvent.Type.AUDIO_MUTED_CHANGED.action);
intentFilter.addAction(BroadcastEvent.Type.CONFERENCE_TERMINATED.action);
... other events
*/
for (type in BroadcastEvent.Type.values()) {
intentFilter.addAction(type.action)
}
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter)
}
// Example for handling different JitsiMeetSDK events
private fun onBroadcastReceived(intent: Intent?) {
if (intent != null) {
val event = BroadcastEvent(intent)
when (event.type) {
BroadcastEvent.Type.CONFERENCE_JOINED -> Timber.i("Conference Joined with url%s", event.getData().get("url"))
BroadcastEvent.Type.PARTICIPANT_JOINED -> Timber.i("Participant joined%s", event.getData().get("name"))
else -> Timber.i("Received event: %s", event.type)
}
}
}
// Example for sending actions to JitsiMeetSDK
private fun hangUp() {
val hangupBroadcastIntent: Intent = BroadcastIntentHelper.buildHangUpIntent()
LocalBroadcastManager.getInstance(this.applicationContext).sendBroadcast(hangupBroadcastIntent)
}
}
added below dependency.
dependencies {
// Jitsi Meet
implementation "org.jitsi.react:jitsi-meet-sdk:5.1.0"
}
Sharing log information also. this sample I am integrating into kotlin multiplatform project.
please suggest to resolve this issue.
Related
I was developing some Conference application and got confused on how to handle PARTICIPANT_LEFT.action in code. Here is my code
private var roomName: String? = null
private lateinit var anotherUser:String
private var username: String? = SharedUserObject.userID
private var jitsiMeetView: JitsiMeetView? = null
#SuppressLint("LogNotTimber")
#Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_call)
anotherUser = intent.getStringExtra("AnotherUser").toString()
SharedUserObject.checkCallActivity = true
// get data from Two Activities (IncomingCall , CallFrom )
// create jitsi call view
// if (!SharedUserObject.checkCallActivity) {
dataFromIntent
jitsiMeetView = JitsiMeetView(this#CallActivity)
val conferenceOptions = videoChatOptions
jitsiMeetView!!.join(conferenceOptions)
setContentView(jitsiMeetView)
jitsiMeetView!!.listener = this
// }
val intentFilter = IntentFilter()
intentFilter.addAction(BroadcastEvent.Type.PARTICIPANT_LEFT.action)
LocalBroadcastManager.getInstance(this).registerReceiver(BroadcastReceiver(applicationContext), intentFilter)
}
override fun requestPermissions(
strings: Array<String>,
i: Int,
permissionListener: PermissionListener
) {}
// this run when user accept the call and set it busy
#SuppressLint("LogNotTimber")
override fun onConferenceJoined(map: Map<String, Any>) {
SharedUserObject.inCall = true
}
#SuppressLint("LogNotTimber")
// this run when user end the call and set it not busy
override fun onConferenceTerminated(map: Map<String, Any>) {
SharedUserObject.inCall = false
//
SharedUserObject.checkCallActivity = false
jitsiMeetView?.leave()
jitsiMeetView?.dispose()
jitsiMeetView = null
finish()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
override fun onConferenceWillJoin(map: Map<String, Any>) {
//LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(muteBroadcastIntent)
}
private val dataFromIntent: Unit
get() {
roomName = intent.getStringExtra("RoomName").toString()
username = ""
}
// Set call subject here. Connection with jitsi call server and create Call.
private val videoChatOptions: JitsiMeetConferenceOptions
get() {
var videoChatUrl: URL? = null
try {
videoChatUrl = URL("https://meet.jit.si")
} catch (e: Exception) {
e.printStackTrace()
}
val meetUserInfo = JitsiMeetUserInfo()
meetUserInfo.displayName = SharedUserObject.userID
return JitsiMeetConferenceOptions.Builder()
.setServerURL(videoChatUrl)
.setAudioOnly(true)
.setAudioMuted(false)
.setUserInfo(meetUserInfo)
.setSubject(roomName) // Set call subject here. use to display phone number here.
.setRoom(roomName)
.build()
}
override fun onDestroy() {
super.onDestroy()
SharedUserObject.checkCallActivity = false
jitsiMeetView?.leave()
jitsiMeetView?.dispose()
jitsiMeetView = null
JitsiMeetActivityDelegate.onHostDestroy(this)
}
}
I need that when user leaves the Conference then the Conference should end. I tried using BroadcastEvent.Type.PARTICIPANT_LEFT.action
but no success was received. Please guide me if there is some other way to implement it.
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.
so I have an NFC tag that stores a URL and I am building an android app that reads that tag and starts the app to the Main activity. After that I have a WebView that displays that website. My problem here is, each tag has a different url/path (lets say they are clothing items and each point to a product on the website). How do I get the NDEF message from the tag (the url) and pass it to that parameter of the webview? Thank your for your help, here I will leave my code.
I have followed the android documentation on NFC both, basic and advanced, googled every page and searched everything on reddit, however I could not find any answer...
This is my Main Activity, the one that opens as soon as it reads the tag:
class MainActivity : AppCompatActivity() {
private var adapter: NfcAdapter? = null
// Pending intent for NFC intent foreground dispatch.
// Used to read all NDEF tags while the app is running in the foreground.
private var nfcPendingIntent: PendingIntent? = null
// Optional: filter NDEF tags this app receives through the pending intent.
//private var nfcIntentFilters: Array<IntentFilter>? = null
private val KEY_LOG_TEXT = "logText"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initNfcAdapter()
if (intent != null)
{
processIntent(intent)
}
}
private fun initNfcAdapter() {
val nfcManager = getSystemService(Context.NFC_SERVICE) as NfcManager
adapter = nfcManager.defaultAdapter
}
override fun onResume() {
super.onResume()
enableNfcForegroundDispatch()
}
private fun enableNfcForegroundDispatch() {
try {
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val nfcPendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
adapter?.enableForegroundDispatch(this, nfcPendingIntent, null, null)
} catch (ex: IllegalStateException) {
}
}
override fun onPause() {
disableNfcForegroundDispatch()
super.onPause()
}
private fun disableNfcForegroundDispatch() {
try {
adapter?.disableForegroundDispatch(this)
} catch (ex: IllegalStateException) {
}
}
private fun processIntent(checkIntent: Intent) {
if(checkIntent.action == NfcAdapter.ACTION_NDEF_DISCOVERED) {
val rawMessages = checkIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
}
}
}
This is the webview activity:
class Webview : AppCompatActivity() {
private lateinit var webview: Webview1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_webview)
val myWebView: WebView = findViewById(R.id.webview)
myWebView.webViewClient = object : WebViewClient () {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url != null) {
view?.loadUrl(url)
}
return true
}
}
myWebView.loadUrl("https://website.com")
myWebView.settings.javaScriptEnabled=true
myWebView.settings.allowContentAccess=true
myWebView.settings.domStorageEnabled=true
myWebView.settings.useWideViewPort=true
myWebView.settings.setAppCacheEnabled(true)
}
}
I want to pass the NDEF message to myWebVIew.loadUrl().
In my app I have two activities. The main activity that only has a search button in the Appbar and a second, searchable, activity. The second activity hold a fragment that fetches the data searched in it's onCreate call. My problem is that the fragment fetches the data twice. Inspecting the lifecycle of my activities, I concluded that the searchable activity gets paused at some point, which obviously determines the fragment to be recreated. But I have no idea what causes the activity to be paused.
Here are my activities
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val root = binding.root
setContentView(root)
//Setup the app bar
setSupportActionBar(binding.toolbar);
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return initOptionMenu(menu, this)
}
}
fun initOptionMenu(menu: Menu?, context: AppCompatActivity): Boolean {
val inflater = context.menuInflater;
inflater.inflate(R.menu.app_bar_menu, menu)
// Get the SearchView and set the searchable configuration
val searchManager = context.getSystemService(Context.SEARCH_SERVICE) as SearchManager
(menu?.findItem(R.id.app_bar_search)?.actionView as SearchView).apply {
// Assumes current activity is the searchable activity
setSearchableInfo(searchManager.getSearchableInfo(context.componentName))
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
}
return true;
}
SearchActivity.kt
class SearchActivity : AppCompatActivity() {
private lateinit var viewBinding: SearchActivityBinding
private var query: String? = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = SearchActivityBinding.inflate(layoutInflater)
val root = viewBinding.root
setContentView(root)
// Setup app bar
supportActionBar?.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
supportActionBar?.setCustomView(R.layout.search_app_bar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
//Get the query string
if (Intent.ACTION_SEARCH == intent.action) {
intent.getStringExtra(SearchManager.QUERY).also {
//Add the query to the appbar
query = it
updateAppBarQuery(it)
}
}
//Instantiate the fragment
if (savedInstanceState == null) {
val fragment = SearchFragment.newInstance();
val bundle = Bundle();
bundle.putString(Intent.ACTION_SEARCH, query)
fragment.arguments = bundle;
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.commitNow()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return initOptionMenu(menu, this)
}
private fun updateAppBarQuery(q: String?) {
supportActionBar?.customView?.findViewById<TextView>(R.id.query)?.apply {
text = q
}
}
}
As you can see, I am using the built in SearchManger to handle my search action and switching between activities. I haven't seen anywhere in the docs that during search, my searchable activity might get paused or anything like that. Does anyone have any idea why this happens? Thanks in advance!
edit: Here is my onCreate method for the SearchFragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val query = arguments?.getString(Intent.ACTION_SEARCH);
//Create observers
val searchResultObserver = Observer<Array<GoodreadsBook>> {
searchResultListViewAdapter.setData(it)
}
viewModel.getSearchResults().observe(this, searchResultObserver)
GlobalScope.launch { //Perform the search
viewModel.search(query)
}
lifecycle.addObserver(SearchFragmentLifecycleObserver())
}
Here, searchResultListViewAdapter is the adapter for a RecyclerViewand searchResult is a livedata in the view-model holding the search result
Here is the stack trace for the first call of onCreate() on SearchFragment:
And here is for the second call:
Here is the ViewModel for the SearchFragment:
class SearchViewModel() : ViewModel() {
private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
MutableLiveData<Array<GoodreadsBook>>();
}
fun getSearchResults(): LiveData<Array<GoodreadsBook>> {
return searchResults;
}
// TODO: Add pagination
suspend fun search(query: String?) = withContext(Dispatchers.Default) {
val callback: Callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// TODO: Display error message
}
override fun onResponse(call: Call, response: Response) {
// TODO: Check res status
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
// Create the bitmap from the imageUrl
searchResults.postValue(parsedRes)
}
}
launch { searchBook(query, callback) }
}
}
I made some changes to the app since posted this and right now the search doesn't work for some reason in the main branch. This ViewModel it's from a branch closer to the time I posted this. Here is the current ViewModel, although the problem is present in this variant as well:
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
// private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
//// MutableLiveData<Array<GoodreadsBook>>();
//// }
companion object {
private const val SEARCH_RESULTS = "searchResults"
}
fun getSearchResults(): LiveData<Array<GoodreadsBook>> =
savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
// TODO: Add pagination
fun search(query: String?) {
val searchResults = savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
if (searchResults.value == null)
viewModelScope.launch {
withContext(Dispatchers.Default) {
//Handle the API response
val callback: Callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// TODO: Display error message
}
override fun onResponse(call: Call, response: Response) {
// TODO: Check res status
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
searchResults.postValue(parsedRes)
}
}
launch { searchBook(query, callback) }
}
}
}
}
The searchBook function just performs the HTTP request to the API, all the data manipulation is handled in the viewModel
try this way
Fragment sf = SearchFragment.newInstance();
Bundle args = new Bundle();
args.putString(Intent.ACTION_SEARCH, query);
sf.setArguments(args);
getFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, sf).addToBackStack(null).commit();
If your activity is getting paused in between then also onCreate of your activity should not be called and that's where you are instantiating the fragment.i.e Fragment is not created again(view might be created again).
As as you have subscribed live data in onCreate of Fragment it should also not trigger an update(onChanged() won't be called for liveData) again.
Just to be sure about live data is not calling onChanged() again try below (i feel that's the culprit here as i can't see any other update happening)
As you will not want to send the same result to your search page again so distinctUntilChanged is a good check for your case.
viewModel.getSearchResults().distinctUntilChanged().observe(viewLifecycleOwner,
searchResultObserver)
Do subscription of live data in onActivityCreated of
fragment.(reference)
Instead of using globalScope you can use viewModelScope and launch from inside your ViewModel.(just a suggestion for clean code)
And what's SearchFragmentLifecycleObserver?
P.S - If you can share the ViewModel code and how the search callback's are triggering data it will be great.But Current lifecycle should not effect the creation of new fragment.
Use SaveStateHandle in your ViewModel to persist the loaded data, and don't use GlobalContext to do the fetching, encapsulate the fetching in VieModel. GlobalContext should only be used for fire and forget actions, which are not bound the any views or lifecycle.
How your SearchViewModel could look like:
#Parcelize
class SearchResult(
//fields ...
) : Parcelable
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private var isLoading : Boolean = false
fun searchLiveData() : LiveData<SearchResult> = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
fun fetchSearchResultIfNotLoaded() { //do this in onCreate
val liveData = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
if(liveData.value == null) {
if(isLoading)
return
isLoading = true
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO) {
//fetching task
SearchResult()
}
liveData.value = result
isLoading = false
}catch (e : Exception) {
//log
isLoading = false
}
}
}
}
companion object {
private const val EXTRA_SEARCH = "EXTRA_SEARCH"
}
}
And in your Search Fragment onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val searchResultObserver = Observer<Array<GoodreadsBook>> {
searchResultListViewAdapter.setData(it)
}
viewModel.searchLiveData().observe(viewLifeCycleScope, searchResultObserver)
viewModel.fetchSearchResultIfNotLoaded()
}
I think the Android team in charge of the documentation should really do a better job. I went ahead and just removed the SearchManager from the SearchViewand use the onQueryTextListener directly, only to see that with this approach I also get my listener called twice. But thanks to this post, I saw that apparently it's a bug with the emulator (or with the way SearchView handles the submit event). So if I press the OSK enter button everything works as expected.
Thanks everyone for their help!