I have inherited a WearOS app, which needs to use the watch sensors to record health data, such as heart rate, temperature and acceleration. The app is made in Java, is around 7 years old and it doesn't follow any specific architecture.
I want to update the app, to use Kotlin, Jetpack Compose and the recommended Android app architecture (). I have separated most of the code into UI and business logic, but I get stuck at the main functionality, which is the service that is in charge of starting the health trackers.
The service is started when I press a start button, which is placed in a jetpack component. It then runs until the user stops it again. It should be able to run in the background, so the user can have it running for days if need be.
My questions are:
How/where should i initialize the service, so I can start and stop it from my composable?
What is the best practices around services in the mvvm architecture?
The structure or component hierarchy of my app is something like this:
MonitoringService (Service)
MainActivity (FragmentActivity)
-- WearApp (Composable)
--- TimerScreen (Composable)
--- TimerViewModel (AndroidViewModel)
The service is just a tester right now and has a binder, a start and stop function which starts logging every second to simulate recording health data and a state with two booleans to track whether the service is recording or not.
I start and bind the service and create the TimerViewModel in the MainActivity onCreate. The service state is made a mutable stateflow, as it needs to connect and then update the viewmodel. The service and viewmodel is then passed through WearApp, to TimerScreen. This is done in an attempt to connect the service and viewmodel, so I can start and stop recording from TimerScreen. It has somewhat worked but sometimes when I open or restart the app, the service isn't connected.
Here is my MainActivity:
class MainActivity : FragmentActivity() {
private val TAG = "Testing"
private val _serviceState = MutableStateFlow<MonitoringService?>(null)
private val serviceState = _serviceState.asStateFlow()
private var isBound = false
private val viewModel: TimerViewModel by viewModels()
private val monitoringServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.i(TAG, "Service connected")
val binder = service as MonitoringService.LocalBinder
val monitoringService = binder.getService()
_serviceState.value = monitoringService
isBound = true
viewModel.observeService(monitoringService)
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.i(TAG, "Service disconnected")
isBound = false
_serviceState.value = null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.i(TAG, "MainActivity onCreate")
// Start the service first to ensure it stays alive
Intent(this, MonitoringService::class.java).also { intent ->
startService(intent)
}
setContent {
val service by serviceState.collectAsState()
if (service != null) {
WearApp(timerViewModel = viewModel, service = service)
} else {
Loading()
}
}
}
override fun onStart() {
super.onStart()
Log.i(TAG, "MainActivity onStart")
// Bind to the service when activity starts
Intent(this, MonitoringService::class.java).also { intent ->
bindService(intent, monitoringServiceConnection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
Log.i(TAG, "MainActivity onStop")
// Unbind from the service when activity stops
if (isBound) {
unbindService(monitoringServiceConnection)
isBound = false
}
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "MainActivity onDestroy")
}
}
And my MonitoringService:
class MonitoringService : Service() {
private val TAG: String = "Testing"
private val handler = Handler(Looper.getMainLooper())
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val _serviceState =
MutableStateFlow(ServiceState(isRecording = false, isRecordingExtra = false))
val serviceState: StateFlow<ServiceState> = _serviceState.asStateFlow()
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): MonitoringService = this@MonitoringService
}
private val logRunnable = object : Runnable {
override fun run() {
if (_serviceState.value.isRecording) {
Log.i(TAG, "Service is running!")
handler.postDelayed(this, 1000)
}
}
}
private val logExtraRunnable = object : Runnable {
override fun run() {
if (_serviceState.value.isRecordingExtra) {
Log.i(TAG, "Service Extra is running!")
handler.postDelayed(this, 1000)
}
}
}
override fun onCreate() {
super.onCreate()
Log.i(TAG, "MonitoringService onCreate")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "MonitoringService onStartCommand")
// If service is killed and restarted, this ensures it restarts
return START_STICKY
}
override fun onBind(intent: Intent): IBinder {
Log.i(TAG, "MonitoringService onBind")
return binder
}
override fun onRebind(intent: Intent?) {
Log.i(TAG, "MonitoringService onRebind")
super.onRebind(intent)
}
override fun onUnbind(intent: Intent?): Boolean {
Log.i(TAG, "MonitoringService onUnbind")
// Allow rebinding
return true
}
fun startLogging() {
Log.i(TAG, "Logging started")
_serviceState.value = _serviceState.value.copy(isRecording = true)
handler.post(logRunnable)
}
fun stopLogging() {
Log.i(TAG, "Logging stopped")
_serviceState.value = _serviceState.value.copy(isRecording = false)
handler.removeCallbacks(logRunnable)
}
fun startRecordingExtra() {
if (!_serviceState.value.isRecordingExtra) {
Log.i(TAG, "Logging extra")
_serviceState.value = _serviceState.value.copy(isRecordingExtra = true)
handler.post(logExtraRunnable)
serviceScope.launch {
delay(5000L)
stopRecordingExtra()
}
}
}
private fun stopRecordingExtra() {
_serviceState.value = _serviceState.value.copy(isRecordingExtra = false)
handler.removeCallbacks(logExtraRunnable)
Log.i(TAG, "Stopped Extra Recording")
}
override fun onDestroy() {
super.onDestroy()
stopLogging()
stopRecordingExtra()
serviceScope.cancel()
Log.i(TAG, "MonitoringService onDestroy")
}
}
I have tried to look at other posts like:
- Android Architecture Components ViewModel - communication with Service/IntentService
- BoundService + LiveData + ViewModel best practice in new Android recommended architecture
But everything I find seems outdated.
I hope it all makes sense and that someone can help me out.