Skip to content

Commit 7d7f7a4

Browse files
committed
Fix errors with boolean types in method return and flow types on iOS
Fixes "_TypeError (type 'int' is not a subtype of type 'bool?' in type cast)". We now serialize boolean values because they are ints when received via Kotlin -> Dart leading to these casting errors. For simplicity the serialization is added for both directions and for Android and iOS, although only Kotlin -> Dart and iOS would be necessary.
1 parent 21826db commit 7d7f7a4

File tree

7 files changed

+152
-20
lines changed

7 files changed

+152
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## unreleased
44

5+
- Fix errors with boolean types in method return and flow types on iOS
6+
57
## v0.1.0-rc.1
68

79
- Update Kotlin to 2.1.0 and KSP to 2.0.21-1.0.25

example/flutter/example/lib/main.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class _MyAppState extends State<MyApp> {
2323
late StreamSubscription<int> _subscription2;
2424
late StreamSubscription<int?> _intStateSubscription;
2525
late StreamSubscription<int?> _intStateAddSubscription;
26+
late StreamSubscription<bool?> _boolStateSubscription;
27+
late StreamSubscription<bool> _boolEventsSubscription;
2628
late StreamSubscription<MyDataClass?> _dataClassStateSubscription;
2729
late StreamSubscription<MyDataClass?> _parameterizedDataClassFlowSubscription;
2830
late StreamSubscription<MyDataClass> _dataClassEventsSubscription;
@@ -75,6 +77,7 @@ class _MyAppState extends State<MyApp> {
7577
print(await myTestModule.nullableStringMethod(null));
7678
print(await myTestModule.intMethod(1));
7779
print(await myTestModule.doubleMethod(1.0));
80+
print(await myTestModule.boolMethod(false));
7881
await myTestModule.parameterizedMethod("dwa", 123, false, 213.3);
7982

8083
await myTestModule.suspendUnitMethod();
@@ -166,6 +169,14 @@ class _MyAppState extends State<MyApp> {
166169
print("int state add value: $item");
167170
});
168171

172+
_boolStateSubscription = myTestModule.boolState((item) {
173+
print("boolean state flow value: $item");
174+
});
175+
176+
_boolEventsSubscription = myTestModule.boolEvents.listen((item) {
177+
print("boolean event: $item");
178+
});
179+
169180
_dataClassStateSubscription = myTestModule.dataClassState((item) {
170181
print("data class state value: $item");
171182
});
@@ -196,6 +207,8 @@ class _MyAppState extends State<MyApp> {
196207
_subscription2.cancel();
197208
_intStateSubscription.cancel();
198209
_intStateAddSubscription.cancel();
210+
_boolStateSubscription.cancel();
211+
_boolEventsSubscription.cancel();
199212
_dataClassStateSubscription.cancel();
200213
_parameterizedDataClassFlowSubscription.cancel();
201214
_dataClassEventsSubscription.cancel();

example/flutter/lib/MyTestModule.dart

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ class MyTestModule {
1313
final Stream<int> intEvents = const EventChannel('MyTestModule_intEvents')
1414
.receiveBroadcastStream()
1515
.map((event) => jsonDecode(event) as int);
16+
final Stream<bool> boolEvents = const EventChannel('MyTestModule_boolEvents')
17+
.receiveBroadcastStream()
18+
.map((event) => jsonDecode(event) as bool);
1619
final Stream<MyDataClass> dataClassEvents = const EventChannel('MyTestModule_dataClassEvents')
1720
.receiveBroadcastStream()
1821
.map((event) => MyDataClass.fromJson(jsonDecode(event) as Map<String, dynamic>));
@@ -119,6 +122,41 @@ final Stream<MyDataClass> dataClassEvents = const EventChannel('MyTestModule_dat
119122

120123
streamController.onListen = startEmittingValues;
121124

125+
return streamController.stream.listen(onData);
126+
}
127+
StreamSubscription<bool?> boolState(Function(bool?) onData) {
128+
final streamController = StreamController<bool?>();
129+
130+
131+
Future<String?> next(String? previous) async {
132+
return await methodChannelToNative.invokeMethod<String>(
133+
'boolState',
134+
[previous]
135+
);
136+
}
137+
138+
void startEmittingValues() async {
139+
String? currentValue;
140+
while (!streamController.isClosed) {
141+
try {
142+
currentValue = await next(currentValue);
143+
if (!streamController.isClosed) {
144+
if (currentValue == null) {
145+
streamController.add(null);
146+
} else {
147+
streamController.add(jsonDecode(currentValue) as bool);
148+
}
149+
}
150+
} catch (e) {
151+
if (!streamController.isClosed) {
152+
streamController.addError(e);
153+
}
154+
}
155+
}
156+
}
157+
158+
streamController.onListen = startEmittingValues;
159+
122160
return streamController.stream.listen(onData);
123161
}
124162
StreamSubscription<int?> intStateAdd(int num, Function(int?) onData) {
@@ -229,9 +267,24 @@ Future<double> doubleMethod(double value) async {
229267

230268
return result;
231269
}
270+
Future<bool> boolMethod(bool value) async {
271+
final valueSerialized = value.toString();
272+
final invokeResult = await methodChannelToNative.invokeMethod<String>(
273+
'boolMethod',
274+
[valueSerialized],
275+
);
276+
277+
if (invokeResult == null) {
278+
throw PlatformException(code: '1', message: 'Method boolMethod failed');
279+
}
280+
281+
final result = jsonDecode(invokeResult) as bool;
282+
283+
return result;
284+
}
232285
Future<void> parameterizedMethod(String a, int b, bool c, double d) async {
233-
234-
await methodChannelToNative.invokeMethod<void>('parameterizedMethod', [a, b, c, d]);
286+
final cSerialized = c.toString();
287+
await methodChannelToNative.invokeMethod<void>('parameterizedMethod', [a, b, cSerialized, d]);
235288
}
236289
Future<DateTime> localDateTimeMethod(DateTime localDateTime) async {
237290
if (localDateTime.isUtc) throw ArgumentError('localDateTime must not be in UTC');

example/src/commonMain/kotlin/com/example/flutterkmpexample/MyTestModule.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import de.voize.flutterkmp.annotation.FlutterModule
66
import de.voize.flutterkmp.annotation.FlutterStateFlow
77
import kotlinx.coroutines.CoroutineScope
88
import kotlinx.coroutines.Dispatchers
9-
import kotlinx.coroutines.GlobalScope
109
import kotlinx.coroutines.delay
1110
import kotlinx.coroutines.flow.Flow
1211
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -27,13 +26,15 @@ val SharedCoroutineScope = CoroutineScope(Dispatchers.Default)
2726
@FlutterModule("MyTestModule")
2827
class MyTestModule(coroutineScope: CoroutineScope) {
2928
private val _intStateFlow = MutableStateFlow(0)
29+
private val _boolStateFlow = MutableStateFlow(false)
3030
private val _dataClassStateFlow = MutableStateFlow(myDataClassInstance)
3131
private val _dataClassSharedFlow = MutableSharedFlow<MyDataClass>()
3232

3333
init {
3434
coroutineScope.launch {
3535
while (true) {
3636
delay(2.seconds)
37+
_boolStateFlow.value = !_boolStateFlow.value
3738
_intStateFlow.value++
3839
_dataClassSharedFlow.emit(myDataClassInstance)
3940
}
@@ -60,6 +61,9 @@ class MyTestModule(coroutineScope: CoroutineScope) {
6061
@FlutterMethod
6162
fun doubleMethod(value: Double): Double = value
6263

64+
@FlutterMethod
65+
fun boolMethod(value: Boolean): Boolean = value
66+
6367
@FlutterMethod
6468
fun parameterizedMethod(
6569
a: String,
@@ -197,6 +201,9 @@ class MyTestModule(coroutineScope: CoroutineScope) {
197201
@FlutterFlow
198202
val intEvents: Flow<Int> = _intStateFlow
199203

204+
@FlutterFlow
205+
val boolEvents: Flow<Boolean> = _boolStateFlow
206+
200207
@FlutterFlow
201208
val dataClassEvents: Flow<MyDataClass> = _dataClassSharedFlow
202209

@@ -209,6 +216,9 @@ class MyTestModule(coroutineScope: CoroutineScope) {
209216
@FlutterStateFlow
210217
fun parameterizedDataClassState(data: MyDataClass): Flow<MyDataClass> = _dataClassStateFlow
211218

219+
@FlutterStateFlow
220+
val boolState: Flow<Boolean> = _boolStateFlow
221+
212222
@FlutterStateFlow
213223
fun intStateAdd(num: Int): Flow<Int> = _intStateFlow.map { it + num }
214224
}

flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ final Stream<${flowTypeArgument.toTypeName()}> $propertyName = const EventChanne
104104
}
105105

106106
val intermediateType = if (flowTypeArgument.requiresSerialization()) {
107-
DartType.Primitive("String")
107+
DartType.Primitive.String()
108108
} else flowTypeArgument
109109

110110
val invokeMethodArguments = listOf("previous") + dartParameters.map { (varName, type) ->

flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/DartType.kt

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,41 @@ internal sealed class DartType {
1111
return if (isNullable) "?" else ""
1212
}
1313

14-
data class Primitive(
15-
val typeName: String,
16-
override val isNullable: Boolean = false,
17-
) : DartType() {
18-
override fun nullable(isNullable: Boolean) =
19-
this.copy(isNullable = isNullable)
20-
override fun toTypeName() = typeName + markIfNullable()
14+
sealed class Primitive : DartType() {
15+
data class String(
16+
override val isNullable: Boolean = false,
17+
) : Primitive() {
18+
override fun nullable(isNullable: Boolean) = this.copy(isNullable = isNullable)
19+
override fun toTypeName() = "String" + markIfNullable()
20+
}
21+
22+
data class Int(
23+
override val isNullable: Boolean = false,
24+
) : Primitive() {
25+
override fun nullable(isNullable: Boolean) = this.copy(isNullable = isNullable)
26+
override fun toTypeName() = "int" + markIfNullable()
27+
}
28+
29+
data class Float(
30+
override val isNullable: Boolean = false,
31+
) : Primitive() {
32+
override fun nullable(isNullable: Boolean) = this.copy(isNullable = isNullable)
33+
override fun toTypeName() = "double" + markIfNullable()
34+
}
35+
36+
data class Double(
37+
override val isNullable: Boolean = false,
38+
) : Primitive() {
39+
override fun nullable(isNullable: Boolean) = this.copy(isNullable = isNullable)
40+
override fun toTypeName() = "double" + markIfNullable()
41+
}
42+
43+
data class Bool(
44+
override val isNullable: Boolean = false,
45+
) : Primitive() {
46+
override fun nullable(isNullable: Boolean) = this.copy(isNullable = isNullable)
47+
override fun toTypeName() = "bool" + markIfNullable()
48+
}
2149
}
2250

2351
data class DateTime(
@@ -88,7 +116,18 @@ internal sealed class DartType {
88116

89117
internal fun DartType.requiresSerialization(): Boolean {
90118
return when (this) {
91-
is DartType.Primitive -> false
119+
is DartType.Primitive -> when (this) {
120+
is DartType.Primitive.Bool -> {
121+
// Booleans are translated to Ints during Kotlin -> ObjC -> Dart interop (although not in the other direction),
122+
// leading to errors like "_TypeError (type 'int' is not a subtype of type 'bool?' in type cast)"
123+
// we therefore serialize them to strings.
124+
true
125+
}
126+
is DartType.Primitive.Double -> false
127+
is DartType.Primitive.Float -> false
128+
is DartType.Primitive.Int -> false
129+
is DartType.Primitive.String -> false
130+
}
92131
is DartType.Class,
93132
is DartType.List,
94133
is DartType.Map,
@@ -108,17 +147,17 @@ internal fun KSType.resolveTypeArgument(index: Int): KSType {
108147

109148
internal fun KSType.toDartType(): DartType {
110149
return when (this.declaration.qualifiedName?.asString()) {
111-
"kotlin.String" -> DartType.Primitive("String", this.isMarkedNullable)
112-
"kotlin.Int" -> DartType.Primitive("int", this.isMarkedNullable)
113-
"kotlin.Float" -> DartType.Primitive("double", this.isMarkedNullable)
114-
"kotlin.Double" -> DartType.Primitive("double", this.isMarkedNullable)
115-
"kotlin.Boolean" -> DartType.Primitive("bool", this.isMarkedNullable)
150+
"kotlin.String" -> DartType.Primitive.String(this.isMarkedNullable)
151+
"kotlin.Int" -> DartType.Primitive.Int(this.isMarkedNullable)
152+
"kotlin.Float" -> DartType.Primitive.Double(this.isMarkedNullable)
153+
"kotlin.Double" -> DartType.Primitive.Double(this.isMarkedNullable)
154+
"kotlin.Boolean" -> DartType.Primitive.Bool(this.isMarkedNullable)
116155
"kotlin.Array", "kotlin.collections.List", "kotlin.collections.Set" -> {
117156
DartType.List(resolveTypeArgument(0).toDartType(), this.isMarkedNullable)
118157
}
119158
"kotlin.collections.Map" -> DartType.Map(
120159
resolveTypeArgument(0).toDartType().also {
121-
require(it is DartType.Primitive && it.typeName == "String") {
160+
require(it is DartType.Primitive.String) {
122161
"Key type of Map can only be String"
123162
}
124163
},

flutter-kmp-ksp/src/main/kotlin/de/voize/flutterkmp/ksp/processor/utils.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ fun KSDeclaration.requiresSerialization(): Boolean {
166166
"kotlin.collections.List",
167167
"kotlin.collections.Map",
168168
"kotlin.collections.Set",
169+
// Booleans are translated to Ints during Kotlin -> ObjC -> Dart interop (although not in the other direction),
170+
// leading to errors like "_TypeError (type 'int' is not a subtype of type 'bool?' in type cast)"
171+
// we therefore serialize them to strings.
172+
"kotlin.Boolean",
169173
)
170174

171175
return qualifiedName?.asString() in types
@@ -277,7 +281,12 @@ return MapEntry(key, ${valueType.getNestedDartDeserializationStatement("value")}
277281
}
278282

279283
internal fun DartType.getDartDeserializationStatement(varName: String, deserializePrimitive: Boolean = false): String = when (this) {
280-
is DartType.Primitive -> if (deserializePrimitive) { "jsonDecode($varName) as ${this.toTypeName()}" } else varName
284+
is DartType.Primitive -> if (
285+
// Booleans always need deserialization
286+
deserializePrimitive || this is DartType.Primitive.Bool
287+
) {
288+
"jsonDecode($varName) as ${this.toTypeName()}"
289+
} else varName
281290
is DartType.DateTime, is DartType.LocalDateTime, is DartType.LocalDate -> "DateTime.parse($varName)"
282291
is DartType.Duration -> "parseIso8601Duration($varName)"
283292
is DartType.TimeOfDay -> "TimeOfDay.fromDateTime(DateTime.parse(\"1998-01-01T${'$'}$varName:00.000\"))"
@@ -330,7 +339,13 @@ internal fun DartType.getDartSerializationStatement(varName: String): String? {
330339
is DartType.LocalDate -> "if ($varName.isUtc) throw ArgumentError('$varName must not be in UTC');\n"
331340
else -> ""
332341
} + "final ${varName}Serialized = " + when (this) {
333-
is DartType.Primitive -> return null
342+
is DartType.Primitive -> when (this) {
343+
is DartType.Primitive.Bool -> "${varName}.toString()"
344+
is DartType.Primitive.Double,
345+
is DartType.Primitive.Float,
346+
is DartType.Primitive.Int,
347+
is DartType.Primitive.String -> return null
348+
}
334349
is DartType.Duration -> "${varName}.toIso8601String()"
335350
is DartType.TimeOfDay -> "\"${'$'}{$varName.hour.toString().padLeft(2, '0')}:${'$'}{$varName.minute.toString().padLeft(2, '0')}\""
336351
is DartType.LocalDate -> "${varName}.toIso8601String().split('T').first"

0 commit comments

Comments
 (0)