Android reconnect to WebSocket with callbackFlow - android

I am working on a multiplayer game server. To do that I implemented an okhttp websocket. I use callbackFlow to handle the callback functions like onMessage or onFailure. This is my repo code:
class WebSocketRepository {
fun socketEventsFlow(): Flow<GameServerResponse> = callbackFlow {
val socketListener = object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
val message = Gson().fromJson(text, GameServerResponse::class.java)
trySendBlocking(message)
}
}
attachWebSocketListener(socketListener)
awaitClose {
socket.close(1000, "application in Background")
}
}
private fun attachWebSocketListener(listener: WebSocketListener) {
val client = OkHttpClient()
val request = Request
.Builder()
.url("ws://10.0.2.2:8080")
.build()
socket = client.newWebSocket(request, listener)
}
}
I then forward pass this Flow through the viewModel to the Activity to for example launch a new fragment. This is the viewModel's code:
class MainActivityViewModel #Inject constructor(
private val repository: WebSocketRepository
) : ViewModel() {
val events = repository.socketEventsFlow()
}
I finally collect the flow in the activity like this:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.events.collect { event ->
//Do stuff here
}
}
}
}
}
There are two problems though.
If the user closes the app (in the background, not fully terminate it), the socket is closed due to the awaitClose statement. I could easily fix that by setting Lifecycle.state.STARTED to Lifecycle.state.CREATED. I worry though that if I do that I would run into problems. For example navigating while the app is in the background. I don't want to create a new socket every time the user closes the app though.
The second problem is the "more important" one. How do I correctly handle reconncetions with this approach? If for example the internet connections drops the server will detect that the client is not there anymore and terminate the connection. I want the app to try to automatically reconnect though. But I have no clue how to implement that. I believe that I would somehow have to re-collect the viewModel's event Flow. But I have no idea on how to actually do that. Is there a better way to handle reconnections?

Related

Livedata and ViewModel update infinite number of times

when i try update model to room database. i expect it just update one time, but it update infinite number of times.
DAO
#Dao
interface UserDAO {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUserDAO(userModel : UserModel)
#Update
suspend fun updateUserDAO(userModel : UserModel)
#Query("select * from user_table where _id =:id")
fun readUserById(id : String) : LiveData<UserModel>
Repository
class UserRepository(private val userAPI: UserAPI, context : Context) {
private val userDAO : UserDAO
init {
val userDatabase: UserRoomDatabase = UserRoomDatabase.getInstance(context)
userDAO = userDatabase.getUserDao()
}
fun readUserByIdDAO(id : String): LiveData<UserModel> = userDAO.readUserById(id)
suspend fun insertUserDAO(userModel : UserModel) = userDAO.insertUserDAO(userModel)
suspend fun updateUserDAO(userModel : UserModel) = userDAO.updateUserDAO(userModel)
}
ViewModel
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
val userModelResponse: MutableLiveData<Resource<UserModel>> = MutableLiveData()
val listUserModelResponse: MutableLiveData<Resource<ArrayList<UserModel>>> = MutableLiveData()
fun readUserByIdDAO(id : String) : LiveData<UserModel> = userRepository.readUserByIdDAO(id)
fun insertUserDAO(userModel: UserModel) = viewModelScope.launch {
userRepository.insertUserDAO(userModel)
}
fun updateUserDAO(userModel: UserModel) = viewModelScope.launch {
userRepository.updateUserDAO(userModel)
}
}
then i use it in MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
private lateinit var binding: ActivityMainBinding
private lateinit var auth : FirebaseAuth
private var userId = ""
override fun onStart() {
super.onStart()
val navController = findNavController(R.id.fragmentHome)
binding.bottomNavigationHome.setupWithNavController(navController)
val userAPI = UserAPI.getInstance()
val userRepository = UserRepository(userAPI,this)
viewModel =
ViewModelProvider(this, UserModelFactory(userRepository)).get(UserViewModel::class.java)
auth = Firebase.auth
userId = auth.currentUser!!.uid
viewModel.readUserByIdDAO(userId).observe(this,{ userFromDAO ->
userFromDAO.active = true
viewModel.updateUserDAO(userFromDAO)
Log.e(TAG,userFromDAO.toString())
})
}
override fun onStop() {
super.onStop()
Log.e(TAG,"Stop")
viewModel.readUserByIdDAO(userId).observe(this,{ userFromDAO ->
userFromDAO.active = false
viewModel.updateUserDAO(userFromDAO)
viewModel.updateUserAPI(userFromDAO)
})
}
what is my problem and how can i fix it?
Every day offers us many surprises; today I wish you all to have a spectacular day. I wish you all the best
The real question is, what do you want to do? If you update the database only at onstart and onstop why do you need livedata? You said in your comment you want to check user online or offline. Why did you write it to the database? Tell us what you want to achieve and we can find another solution..
BTW, this is my answer if you want to check user is online or not:
If you want to check user online or offline why don't you just check
is device online or offline? You can see how to do that here:
How to check if an android device is online
The above link tells you how to make a function to check your
connectivity status. If you want to do something automatically every
time the network state changes, you need to run that function to check
your connectivity every time while your app running. That is not
effective. You can use work manager if you want to automatically
detect network changes:
How to trigger work manager when wifi is connected in android?
So if you know when you want to check the network status, use the
first method. If you want to monitor network status and automatically
do something when status changes, use the second method.
Use an if statement so you won’t repeatedly write the same item back
if (!userFromDAO.active) {
userFromDAO.active = true
viewModel.updateUserDAO(userFromDAO)
}
Log.e(TAG,userFromDAO.toString())

ViewModel Fragment Recreates On Screen Rotation

I'm building an application with latest android architecture components. I'm using firebase firestore as a database with jetpack navigation(Bottom nav). I'm successfully able to display data from DB. But Whenever I rotate mt screen the store fragment recreates & makes request to DB.
Repo
override fun getAllStores() = callbackFlow<State<List<Store>>> {
// Emit loading state
send(State.loading())
val listener = remoteDB.collection(Constants.COLLECTION_STORES)
.addSnapshotListener { querySnapshot, exception ->
querySnapshot?.toObjects(Store::class.java)?.let { store ->
// Emit success state with data
offer(State.success(store))
}
exception?.let {
// emit exception with message
offer(State.failed(it.message!!))
cancel()
}
}
awaitClose {
listener.remove()
cancel()
}
}.catch {
// Thrown exception on State Failed
emit(State.failed(it.message.toString()))
}.flowOn(Dispatchers.IO)
ViewModel
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreViewModel(private val repository: DBInterface = Repo()) : ViewModel() {
fun getAllStores() = repository.getAllStores()
}
Store Fragment
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreFragment : Fragment(R.layout.fragment_store) {
private lateinit var storeAdapter: StoreAdapter
private val viewModel: StoreViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as MainActivity).supportActionBar?.title = getString(R.string.store_title)
setUpRV()
// get all stores
lifecycleScope.launch {
getAllStores()
}
}
private suspend fun getAllStores() {
viewModel.getAllStores().collect { state ->
when (state) {
is State.Loading -> {
store_progress.show()
}
is State.Success -> {
storeAdapter.differ.submitList(state.data)
store_progress.animate().alpha(0f)
.withEndAction {
store_rv.animate().alpha(1f)
store_progress.hide()
}
}
is State.Failed -> {
store_progress.hide()
activity?.toast("Failed! ${state.message}")
}
}
}
}
private fun setUpRV() {
storeAdapter = StoreAdapter()
store_rv.apply {
adapter = storeAdapter
addItemDecoration(SpacesItemDecorator(16))
}
}
}
Main activity(Nav graph)
#InternalCoroutinesApi
#ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
// init bottom navigation
bottom_navigation.setupWithNavController(nav_host_fragment.findNavController())
}
}
Every time it recreates my fragment. I don't want to save or retain any views using methods. Because ViewModel used to protect view on screen rotation. Kindly let me know any tips & tricks. Thanks in advance ;)
Flow in itself is not stateful - that is a key difference between it and LiveData. That means that after your collect completes, the next collect starts the callbackFlow from scratch.
This is precisely why the lifecycle-livedata-ktx artifact contains the asLiveData() extension that allows you to continue to use a Flow at the repository layer while maintaining the stateful (and Lifecycle) properties of LiveData for your UI:
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreViewModel(private val repository: DBInterface = Repo()) : ViewModel() {
fun getAllStores() = repository.getAllStores().asLiveData()
}
You'd change your UI code to continue to use LiveData and observe().
Kotlin is working on a shareIn operation that would allow your ViewModel to save the state of a Flow. That would allow you to use Flow at all layers of your app without requerying information from scratch when the Fragment/Activity that is calling collect gets destroyed and recreated.
you can add android:configChanges="orientation|screenSize|screenLayout" to your manifest for the activity. this should prevents restarts when orientation changes.
check this site and also here are som infos.

Coroutine Worker gets killed after kill the app

Lets say that i have an activity that starts a worker. inside the worker i do a pseudo suspend proccess and then i print out a result from the database. Here is the code
The activity which starts the worker is
class SplashActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
val oneTimeRequest = OneTimeWorkRequest.Builder(MyWorker::class.java).setInputData(Data.Builder().apply {
putInt("data", 1)
}.build()).addTag("worktag").build()
WorkManager.getInstance(applicationContext).enqueue(oneTimeRequest)
}
}
The worker is the below
class MyWorker #AssistedInject constructor(
#Assisted private val appContext: Context,
#Assisted private val params: WorkerParameters,
private val serverRepository: ServerRepository
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
GlobalScope.launch {
for (i in 0..10) {
println("$i")
delay(1000)
}
val servers = serverRepository.getServers()
runOnUiThread {
Toast.makeText(appContext, "${servers.firstOrNull()?.serverAddress}", Toast.LENGTH_SHORT).show()
}
}
return Result.success()
}
}
So the result is that i see in the logcat the system.out with 1,2,3... and then i see a toast messages.
However, when i totally kill the app from the recent while the counter still counts, i never see the toast message.
Why is this happening since i have a GlobalScope coroutine?
And what is the right way to do this??
I was trying to achieve a similar goal. I managed my work by using ForegroundService.
You can find more here
https://developer.android.com/guide/components/foreground-services

Sometimes, ConflatedBroadcastChannel fires recent value without any action

In Google's official codelab about advanced-coroutines-codelab sample, they've used ConflatedBroadcastChannel to watch a variable/object change.
I've used the same technique in one of my side projects, and when resuming the listening activity, sometimes ConflatedBroadcastChannel fires it's recent value, causing the execution of flatMapLatest body without any change.
I think this is happening while the system collects the garbage since I can reproduce this issue by calling System.gc() from another activity.
Here's the code
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val tvCount = findViewById<TextView>(R.id.tv_count)
viewModel.count.observe(this, Observer {
tvCount.text = it
Toast.makeText(this, "Incremented", Toast.LENGTH_LONG).show();
})
findViewById<Button>(R.id.b_inc).setOnClickListener {
viewModel.increment()
}
findViewById<Button>(R.id.b_detail).setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
}
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
companion object {
val TAG = MainViewModel::class.java.simpleName
}
class IncrementRequest
private var tempCount = 0
private val requestChannel = ConflatedBroadcastChannel<IncrementRequest>()
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
.asLiveData()
fun increment() {
requestChannel.offer(IncrementRequest())
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val button = findViewById<Button>(R.id.b_gc)
val timer = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
button.isEnabled = true
button.text = "CALL SYSTEM.GC() AND CLOSE ACTIVITY"
}
override fun onTick(millisUntilFinished: Long) {
button.text = "${TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)} second(s)"
}
}
button.setOnClickListener {
System.gc()
finish()
}
timer.start()
}
}
Here's the full source code :
CoroutinesFlowTest.zip
Why is this happening?
What am I missing?
Quoting from the official response, (The simple and straightforward solution)
The problem here is that you are trying to use
ConflatedBroadcastChannel for events, while it is designed to
represent current state as shown in the codelab. Every time the
downstream LiveData is reactivated it receives the most recent state
and performs the incrementing action. Don't use
ConflatedBroadcastChannel for events.
To fix it, you can replace ConflatedBroadcastChannel with
BroadcastChannel<IncrementRequest>(1) (non-conflated channel, which is
Ok for events to use) and it'll work as you expect it too.
In addition to the answer of Kiskae:
This might not be your case, but you can try to use BroadcastChannel(1).asFlow().conflate on a receiver side, but in my case it led to a bug where the code on a receiver side didn't get triggered sometimes (I think because conflate works in a separate coroutine or something).
Or you can use a custom version of stateless ConflatedBroadcastChannel (found here).
class StatelessBroadcastChannel<T> constructor(
private val broadcast: BroadcastChannel<T> = ConflatedBroadcastChannel()
) : BroadcastChannel<T> by broadcast {
override fun openSubscription(): ReceiveChannel<T> = broadcast
.openSubscription()
.apply { poll() }
}
On Coroutine 1.4.2 and Kotlin 1.4.31
Without using live data
private var tempCount = 0
private val requestChannel = BroadcastChannel<IncrementRequest>(Channel.CONFLATED)
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
Use Flow and Coroutine
lifecycleScope.launchWhenStarted {
viewModel.count.collect {
tvCount.text = it
Toast.makeText(this#MainActivity, "Incremented", Toast.LENGTH_SHORT).show()
}
}
Without using BroadcastChannel
private var tempCount = 0
private val requestChannel = MutableStateFlow("")
val count: StateFlow<String> = requestChannel
fun increment() {
tempCount += 1
requestChannel.value = "Number is $tempCount"
}
The reason is very simple, ViewModels can persist outside of the lifecycle of Activities. By moving to another activity and garbagecollecting you're disposing of the original MainActivity but keeping the original MainViewModel.
Then when you return from DetailActivity it recreates MainActivity but reuses the viewmodel, which still has the broadcastchannel with a last known value, triggering the callback when count.observe is called.
If you add logging to observe the onCreate and onDestroy methods of the activity you should see the lifecycle getting advanced, while the viewmodel should only be created once.

is observeForever lifecycle aware?

I'm working with MVVM, and I have made different implementations of it, but one thing that is still making me doubt is how do I get data from a Repository (Firebase) from my ViewModel without attaching any lifecycle to the ViewModel.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea because I think I should communicate from my repository to my ViewModel either with a callback or a Transformation.
I leave here an example where I fetch a device from Firebase and update my UI, if we can see here, I'm observing the data coming from the repo from the UI, but from the ViewModel I'm also observing data from the repo, and here is where I really doubt if I'm using the right approach, since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
UI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
observeData(deviceId)
}
}
fun observeData(deviceId:String){
viewModel.fetchDeviceData(deviceId).observe(this, Observer {
textView.text = "Tipo: ${it.devType}"
})
ViewModel
class MainViewmodel: ViewModel() {
private val repo = Repo()
fun fetchDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
repo.getDeviceData(deviceId).observeForever {
mutableData.value = it
}
return mutableData
}
}
Repository
class Repo {
private val db = FirebaseDatabase.getInstance().reference
fun getDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
db.child(deviceId).child("config/device").addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
})
return mutableData
}
}
This example just shows how to fetch the device from Firebase, it works, but from my ViewModel, it keeps making me think that observeForever() is not what I'm looking for to communicate data between the repository to the ViewModel.
I have seen Transformations, but I, in this case, I just need to deliver the entire Device object to my UI, so I don't need to transform the Object I'm retrieving to another Object
What should be here the right approach to communicate the repository and the ViewModel properly?
is observeForever lifecycle aware?
No, that's why it's called observeForever.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea
No, it's not, you should be using Transformations.switchMap {.
since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
Well if you're not clearing it in onCleared() using removeObserver(observer), then it won't clear itself, because it observes forever.
here is where I really doubt if I'm using the right approach,
No, you can do much better than this following a reactive approach.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
viewModel.onSelectedDeviceChanged(deviceId)
}
viewModel.selectedDevice.observe(this, Observer { device ->
textView.text = "Tipo: ${device.devType}"
})
}
And
class MainViewModel(
private val savedStateHandle: SavedStateHandle,
): ViewModel() {
private val repo = Repo() // TODO: move to Constructor Argument with ViewModelProvider.Factory
private val selectedDeviceId: MutableLiveData<String> = savedStateHandle.getLiveData<String>("selectedDeviceId")
fun onSelectedDeviceChanged(deviceId: String) {
selectedDeviceId.value = deviceId
}
val selectedDevice = Transformations.switchMap(selectedDeviceId) { deviceId ->
repo.getDeviceData(deviceId)
}
}
And
class Repo {
private val db = FirebaseDatabase.getInstance().reference // TODO: move to constructor arg? Probably
fun getDeviceData(deviceId:String) : LiveData<Device> {
return object: MutableLiveData<Device>() {
private val mutableLiveData = this
private var query: Query? = null
private val listener: ValueEventListener = object: ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableLiveData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
}
override fun onActive() {
query?.removeEventListener(listener)
val query = db.child(deviceId).child("config/device")
this.query = query
query.addValueEventListener(listener)
}
override fun onInactive() {
query?.removeEventListener(listener)
query = null
}
}
}
}
This way, you can observe for changes made in Firebase (and therefore be notified of future changes made to your values) using LiveData, rather than only execute a single fetch and then not be aware of changes made elsewhere to the same data.
To use ObserveForever, you need to remove the observer inside onClear in the ViewModel.
In this case, I would suggest to use Transformation even though you just need a direct mapping without any processing of the data, which is actually the same as what you are doing with the observer for observerForever.
observeForever() is not Lifecycle aware and will continue to run until removeObserver() is called.
In your ViewModel do this instead,
class MainViewmodel: ViewModel() {
private val repo = Repo()
private var deviceData : LiveData<Device>? = null
fun fetchDeviceData(deviceId:String):LiveData<Device>{
deviceData = repo.getDeviceData(deviceId)
return deviceData!!
}
}

Categories

Resources