A WormHole is a speculative structure linking disparate points in spacetime.
In a mobile universe, a WormHole is a special solution of the Einstein field equations, enabling to share platform classes to Flutter, and expose Flutter's classes to your native code.
This library has been created to accelerate (integration of flutter on an existing app)[https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps]
But works with an usual flutter apps, if you don't want to bridge manually your native objects using MethodChannels
I want to add a Flutter view inside my existing Native project,
this screen needs to retrieve an user, and I use in my project MainRepository
I just have to expose my MainRepository
to Flutter through a WormHole !
class MainRepository {
@Expose("retrieveUser")
suspend fun retrieveUser() : User {
return myBDD.getUser()
}
}
val mainRepository = MainRepository()
expose("user", mainRepository)
Can be retrieved in Flutter
@WormHole
abstract class MainRepository {
@Call("retrieveUser")
Future<User> retrieveUser();
factory MainRepository(channelName) => WormHole$MainRepository;
}
final mainRepository = MainRepository("user");
User user = await mainRepository.retrieveUser();
WormHole depends on json_annotation
and needs a dart build_runner
to run json_serializable
and wormhole_generator
dependencies:
flutter:
sdk: flutter
json_annotation: 3.0.0
wormhole: 1.0.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: 1.7.2
json_serializable: 3.2.3
wormhole_generator: 1.0.0
- Add
@Expose("name")
on your method, specifying a method name . For async methods, be sure they're implementing coroutine'ssuspend
. For observables results, be sure they're implementing coroutine'sFlow
class UserManager(val context: Context) {
companion object {
const val USER = "user"
}
/**
* For example, save an user as json into shared preferences
* Can be a room database, etc.
*/
private val gson = Gson()
private val sharedPreferences = context.getSharedPreferences("user_shared", Context.MODE_PRIVATE)
private val userChannel = ConflatedBroadcastChannel<User?>()
init {
updateUser()
}
private fun updateUser() {
val currentUser = sharedPreferences.getString(USER, null)?.let {
gson.fromJson(it, User::class.java)
}
userChannel.offer(currentUser)
}
/**
* A stream exposing the current user
*/
@Expose("getUser")
fun getUser(): Flow<User?> = userChannel.asFlow()
@Expose("saveUser")
suspend fun saveUser(user: User) {
sharedPreferences.edit().putString(USER, gson.toJson(user)).apply()
updateUser()
}
@Expose("clear")
fun clear() {
sharedPreferences.edit().remove(USER).apply()
updateUser()
}
}
- Expose this class to a Flutter's element
class MainActivity : FlutterActivity() {
private val userManager by lazy { UserManager(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
/**
* Expose the user manager to be accessible to Flutter via a WormHole
*/
expose("user", userManager)
}
}
@WormHole()
abstract class UserManager {
@Call("getUser")
Stream<User> getUser();
@Call("saveUser")
Future<void> saveUser(User user);
@Call("clear")
void clear();
factory UserManager(channelName) => WormHole$UserManager(channelName);
}
- Create an abstract class mirroring the Native's element
- Annotate it with
@WormHole()
- For each Native's method, create a Dart method annotated with
@Call("methodname")
. For async methods, be sure they're returning aFuture<type>
. For observables results, be sure they're returining aStream<type>
- Create a factory, jumping to
WormHole$yourclass(channelName);
Don't forget to run build_runner with flutter pub run build_runner build
final UserManager userManager = UserManager("user");
- Then retrieve your native object from the WormHole
StreamBuilder(
stream: userManager.getUser(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
final user = snapshot.data;
...
}
}
);
And use it as an usual flutter class
@WormHole() //A WormHole will be created arount this class
class QuestionBloc implements Bloc {
//this method will be exposed to native through a WormHole, using the method name "ask"
@Expose("ask")
void ask(Question question) {
//TODO your code here
}
expose(channelName) => WormHole$QuestionBloc(channelName).expose(this);
}
I want to expose this object through the wormhole named "question"
final bloc = QuestionBloc();
bloc.expose("question");
- Add
@WormHole
annotation on your class - Add
@Expose("name")
on your method, specifying a method name - Add an expose method calling
WormHole$yourclass("channelName").expose(this)
- Expose your object using
.expose(channel)
Don't forget to run build_runner with flutter pub run build_runner build
interface QuestionBloc {
@Call("ask")
fun question(question: Question)
}
- Create an interface, containing reflecting your Dart class
QuestionBloc
- For each @Expose method in Dart, create an @Call method, containing the same method name :
ask
//retrieve the Flutter's QuestionBloc, in a FlutterActivity for example
val questionBloc = retrieve<QuestionBloc>("question")
- Retrieve an object sent into the wormhole
questionBloc.ask(Question("what's your name"))
- Your can now interact with your class
WormHole uses annotation processing to Expose/Retrieve Dart through WormHole
See Generator for further explanations and configurations
WormHole uses jvm reflection to Expose/Retrieve Java/Kotlin objects to be accessible through WormHole
See WormHole-Android
¯\__(ツ)_/¯
WormHole is not available yet on iOS because :
- Annotation Processor does not exists in swift or ObjectiveC
- Reflection on swift does not allow to perform needed actions
- Swift (without ReactiveSwift) does not provides Futures / Streams
If someone in the community has an idea to port it, don't hesitate to make a pull request !