Mastering Gyroscope Sensor in Android: Expert Tips for Clean Code, with ChatGPT’s Assistance

Badea Mihai Bogdan
6 min readDec 11, 2022
Midjourney generated image

In writing this article, I asked ChatGTP for a sample implementation of Gyroscope sensor access in Android using the Dependency Inversion Principle, and the results are surprising:

Provide me an implementation of a gyroscope in Android with dependency inversion principle using Kotlin

And ChatGPT response:

ChatGPT: Here is an example implementation of a gyroscope in Android using the dependency inversion principle with Kotlin:

// Gyroscope interface
interface Gyroscope {
fun getAngularVelocity(): Float
}

// Gyroscope implementation
class AndroidGyroscope(context: Context): Gyroscope {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val gyroscopeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

override fun getAngularVelocity(): Float {
val sensorEvent = SensorEvent()
sensorManager.registerListener(object : SensorEventListener {
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}

override fun onSensorChanged(event: SensorEvent?) {
sensorEvent.values = event!!.values
}
}, gyroscopeSensor, SensorManager.SENSOR_DELAY_NORMAL)

return sensorEvent.values[0]
}
}

// Usage in some other class
class SomeClass(gyroscope: Gyroscope) {
fun doSomething() {
val angularVelocity = gyroscope.getAngularVelocity()
// ...
}
}

ChatGPT: In this example, the AndroidGyroscope class provides the implementation of the Gyroscope interface. The SomeClass class depends on the Gyroscope interface, but not on any specific implementation of the interface. This allows us to use the dependency inversion principle, which states that high-level modules should not depend on low-level modules, but rather both should depend on abstractions. This makes the code more flexible and easier to maintain.

Needs refactoring

It seems that regarding dependency inversion principle, ChatGPT was straight to the point, but there were some missing sensor result values.

Could you also get all the values from the sensor in a different data class and rename Gyroscope class to GyroscopeReadUseCase?

ChatGPT: Certainly. Here is an updated version of the code that gets all the values from the sensor in a different data class and renames the Gyroscope class to GyroscopeReadUseCase:

// Gyroscope data class
data class GyroscopeData(
val x: Float,
val y: Float,
val z: Float
)

// Gyroscope interface
interface Gyroscope {
fun getGyroscopeData(): GyroscopeData
}

// Gyroscope implementation
class AndroidGyroscope(context: Context): Gyroscope {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val gyroscopeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

override fun getGyroscopeData(): GyroscopeData {
val sensorEvent = SensorEvent()
sensorManager.registerListener(object : SensorEventListener {
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}

override fun onSensorChanged(event: SensorEvent?) {
sensorEvent.values = event!!.values
}
}, gyroscopeSensor, SensorManager.SENSOR_DELAY_NORMAL)

return GyroscopeData(
x = sensorEvent.values[0],
y = sensorEvent.values[1],
z = sensorEvent.values[2]
)
}
}

// Usage in some other class
class SomeClass(gyroscope: Gyroscope) {
fun doSomething() {
val gyroscopeData = gyroscope.getGyroscopeData()
// ...
}
}

In this updated code, the AndroidGyroscope class now provides the implementation of the Gyroscope interface, which has been renamed to GyroscopeReadUseCase. The GyroscopeData class is a data class that holds the values from the gyroscope sensor. The getGyroscopeData() method in the AndroidGyroscope class returns an instance of the GyroscopeData class with the x, y, and z values from the sensor. The SomeClass class still depends on the Gyroscope interface, but not on any specific implementation of the interface. This allows us to use the dependency inversion principle, which helps to make the code more flexible and maintainable.

Refactoring was partially done, additional sensor values have been added, but renaming was not fulfilled, and descriptions are also partially updated.

Refactor with Flow

Data retrieving is unusable in the code provided, so I’ve asked ChatGPT to refactor to Flow.

Use flow as a method for retrieving data

ChatGPT: Here is an updated version of the code that uses Kotlin’s Flow class as a method for retrieving data from the gyroscope sensor:

// Gyroscope data class
data class GyroscopeData(
val x: Float,
val y: Float,
val z: Float
)

// Gyroscope interface
interface Gyroscope {
fun getGyroscopeData(): Flow<GyroscopeData>
}

// Gyroscope implementation
class AndroidGyroscope(context: Context): Gyroscope {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val gyroscopeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

override fun getGyroscopeData(): Flow<GyroscopeData> = callbackFlow {
val sensorEventListener = object : SensorEventListener {
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}

override fun onSensorChanged(event: SensorEvent?) {
event?.let { offer(GyroscopeData(x = it.values[0], y = it.values[1], z = it.values[2])) }
}
}

sensorManager.registerListener(
sensorEventListener,
gyroscopeSensor,
SensorManager.SENSOR_DELAY_NORMAL
)

awaitClose { sensorManager.unregisterListener(sensorEventListener) }
}
}

// Usage in some other class
class SomeClass(gyroscope: Gyroscope) {
fun doSomething() {
gyroscope.getGyroscopeData().collect { gyroscopeData ->
// ...
}
}
}

In this updated code, the AndroidGyroscope class now uses Kotlin’s Flow class to return a stream of GyroscopeData objects from the getGyroscopeData() method. The Flow class is a powerful tool for managing asynchronous data streams in Kotlin. In this example, the Flow is created using the callbackFlow builder, which allows us to provide a suspending function that will be called when a collector is registered. In this case, we register a SensorEventListener with the SensorManager to listen for updates from the gyroscope sensor. When a new event is received, we use the offer method to add the new GyroscopeData to the stream. The awaitClose function is used to unregister the SensorEventListener when the Flow is closed.

In the SomeClass class, we use the collect method on the Flow to consume the stream of GyroscopeData objects. This allows us to perform some action on each GyroscopeData object as it is received from the sensor.

Several corrections to the above code need to be made:

First correction at line:

event?.let { offer(GyroscopeData(x = it.values[0], y = it.values[1], z = it.values[2]))}

we get an error from the compiler Using ‘offer(E): Boolean’ is an error. Deprecated in the favour of ‘trySend’ method - we will use trySendMethod instead.

Second corrections involving SomeClass

class SomeClass(private val gyroscope: Gyroscope) {
suspend fun doSomething() {
gyroscope.getGyroscopeData().collect { gyroscopeData ->
// ...
}
}
}

The Gyroscope constructor parameter was not made as a class variable, and the collect method needed to be a suspending function or to be called from a coroutine. More details about coroutines and Flow can be found in the following pages: https://kotlinlang.org/docs/coroutines-overview.html, https://kotlinlang.org/docs/flow.html

Unit Testing consumer class

I’ve asked ChatGPT to provide a test using a fake test double for Gyroscope:

Generate unit tests by faking AndroidGyroscope

ChatGPT: Here is an example of a unit test that uses a fake implementation of the AndroidGyroscope class:

@Test
fun testAndroidGyroscope() {
val gyroscope = object : Gyroscope {
override fun getGyroscopeData(): Flow<GyroscopeData> = flowOf(GyroscopeData(1f, 2f, 3f))
}

val someClass = SomeClass(gyroscope)
someClass.doSomething()

// No need to verify anything, as we are using a fake implementation
}

The next ChatGPT response is incorrect because we are not testing AndroidGyroscope implementation, since we are providing a test double that implements Gyroscope interface, and testing is for SomeClass. Also above testing method should be renamed to testSomeClass.

In this example, we create an anonymous object that implements the Gyroscope interface. This fake implementation simply returns a Flow of GyroscopeData objects with x, y, and z values of 1, 2, and 3, respectively.

Once we have set up the fake Gyroscope object, we can create an instance of the SomeClass class and call the doSomething() method. This will trigger the call to the getGyroscopeData() method on the fake Gyroscope object.

Since we are using a fake implementation of the Gyroscope interface, there is no need to verify any method calls. This makes the test easier to write and more reliable, as there is no need to use a mocking framework.

These tests can be run using a unit testing framework such as JUnit. They can help to ensure that the AndroidGyroscope class is working as expected, and they can also help to catch any regressions or bugs in the code. By using a fake implementation of the Gyroscope interface, these tests are easier to write and more portable, which can make the testing process more efficient and effective.

Full code

For the final working code, I’ve refactored SomeClass and you can review the full code in the following Github gist: https://gist.github.com/bmbogdan/d55d97421f486dcab4f78192922af13c

Conclusions

The purpose of this article was to describe a clean code implementation of accessing Gyroscope sensor in Android that provides a separation between high-level code such as business logic and low-level code, Android specific objects.

By using Dependency Inversion Principle you will work in your business layer only with an abstraction and can maintain and test it easier without dependencies of Android objects. Possible extensions can be checking for paid subscription in an app that uses the gyroscope or using a third-party library to apply a low-pass filter to raw sensor data. All of these dependencies will be irrelevant to the high-level code.

From the code generation capabilities standpoint, ChatGPT seems very good at providing perfect answers in some cases, and in cases where it does not, you can guide it to refine your code. This technology is great, but you should review the provided responses.

Thank you for reading! You could find more details about Dependency Inversion Principle, Gyroscope and ChatGPT in the following links:

#ChatGPT #Gyroscope #Kotlin #Flow #Coroutines

Also you can find me on Linkedin and Twitter

--

--

Badea Mihai Bogdan

Senior Android Software Engineer with focus on Clean Code, Clean Architecture and TDD.