Important
Toda a documentação do Tuya SDK como configuração do projeto está nos links abaixo.
Developer Tuya
Guia de desenvolvimento para Android-IoT App SDK-Tuya
Platform Channels
MethodChannel Class
Event channel to listen Broadcast receiver’s event
[!IMPORTANT] Lembre da Configuração do TuyaSDK
Lembre-se de configurar corretamente o SDK no app, de acordo com a documentação oficial. Caso não configure corretamente a AppKey, AppSecret e os demais, não conseguirá prosseguir.
Para integrar o código Kotlin com o Flutter será necessário utilizar o Method Channel no Flutter, dessa forma você cria um CHANNEL onde o valor será o mesmo tanto no Flutter quanto no Nativo, e por meio desse CHANNEL eles vão se conversar.
Por exemplo:
No Nativo foi criado um companion object com o CHANNEL e seu valor tuya_integration. Pode ser outro valor.
companion object {
const val CHANNEL = "tuya_integration"
}
Agora no flutter, eu também criei uma constante com o mesmo valor
class Constants {
static const CHANNEL = "tuya_integration";
}
Agora, ainda no Flutter, em alguma classe, ou na classe que irá utilizar, será necessário criar o MethodChannel com o valor para o CHANNEL
static const channel = MethodChannel(Constants.CHANNEL);
Dessa forma consigo utilizar o channel e chamar alguma funcao que está no nativo, como por exemplo:
Utilizando o invokeMethod do channel consigo dizer qual a funcao do nativo que desejo chamar. Neste caso foi a função de nome auth e passo um objeto de chave-valor que a função solicita. Como é um login, preciso passar as informações de email, password e código do país.
_loginTuya() async {
await channel.invokeMethod("auth", <String, String>{
"country_code": "55",
"email": _emailController.text,
"password": _passwController.text
});
}
Lá no código nativo, vamos precisar criar uma função que será chamada quando o channel invocar o método auth.
No Nativo, nossa MainActivity vai herdar da FlutterActivity() que vai nos fornecer um método para configurar a Engine do Flutter, que é o
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
}
Vamos criar uma variavel do tipo MethodChannel e inicializar ela dentro da função configureFlutterEngine lembrando que o CHANNEL passado dentro do MethodChannel é a constante criada no companion object.
private lateinit var channel: MethodChannel
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
}
Dessa forma, podemos utilizando o channel, chamar a função setMethodCallHandler() que vai nos fornecer um call e um result.
- CALL -> dentro do call temos o method que será o método chamado pelo Flutter, por exemplo: Foi chamado a função auth, então o call será auth.
- RESULT -> é a forma de retornarmos algo para o Flutter, como o retorno de uma função.
No caso da função de autenticação, dentro do setMethodCallHandler fica algo assim:
if (call.method == "auth") {
ThingHomeSdk.getUserInstance().loginWithEmail(
countryCode.toString(),
email.toString(),
password.toString(),
object : ILoginCallback {
override fun onSuccess(user: User?) {
user?.let {
result.success(it.uid)
}
}
override fun onError(code: String?, error: String?) {
if (code != null && error != null) {
result.error(code, error, null)
};
}
}
)
}
Isso quer dizer que se o method chamado fo auth ele vai chamar essa função que está dentro. E observe que dentro do callback temos o result, onde da um result.success que retorna o UID do usuário e outro que retorna o código e erro, caso tenha erros.
Isso será feito o tempo inteiro, para cada função chamada.
Outro fator importante para a integração são os argumentos passados quando chamamos a função.
Se observou bem, quando chamamos a função no Flutter passamos alguns argumentos como email, senha, etc.
Para que no Kotlin consigamos pegar esses dados, vamos precisar utilizar o ARGUMENTS que fica dentro do CALL ARGUMENTS será um Map de <String, String> ou <*, *>. Neste caso estamos utilizando apenas como String. E dentro desse Map teremos como pegar cada um dos parametros informados.
val argument = call.arguments as Map<String, String>
val countryCode = argument["country_code"]
val email = argument["email"]
val password = argument["password"]
val code = argument["code"]
Dessa forma, agora consigo pegar o valor passado na função Flutter e passar ele dentro de alguma função no Kotlin.
Para realizar o registro de um usuário, é importante seguir alguns passos.
- Enviar código de registro para email do usuário
- Registrar email e senha do usuário juntamente com o código recebido no email
Outo ponto importante são as informações solicitadas para registro de usuários
- Código do país
- Senha
- Código enviado por email
Então para que o registro aconteça, primeiramente chamamos via o MethodChannel o método para receber o código de registro. Lembrando que deve ser passado juntamente o email e código do país.
Vamos adicionar uma constante chamada SEND_CODE tanto no Nativo quanto no Flutter, para chamarmos por constante, fica mais organizado.
// Kotlin companion object { const val SEND_CODE = "send_code" const val REGISTER = "register" } // Dart class Method { static const SEND_CODE = "send_code"; static const REGISTER = "register"; }No Flutter, vamos chamar a função e passar os parametros necessários
Future<void> _validateTuya() async {
await channel.invokeMethod(
Methods.SEND_CODE, <String, String>{
"country_code": countryCodeController,
"email": emailController
});
}
E no Kotlin, vamos ter a função que será executada quando o method SEND_CODE for chamado.
[!IMPORTANT] Observe que chamamos de dentro do getUserInstance() a função sendVerifyCodeWithUserName() e ela solicita alguns parametros. Caso tenha dúvidas sobre cada um, basta olhar na própria documentação User Doc
if (call.method == SEND_CODE) {
ThingHomeSdk.getUserInstance().sendVerifyCodeWithUserName(
email.toString(),
"",
countryCode.toString(),
1,
object : IResultCallback {
override fun onError(code: String?, error: String?) {
Toast.makeText(context, "Erro: $code - $error", Toast.LENGTH_LONG)
.show()
}
override fun onSuccess() {
Toast.makeText(context, "Codigo enviado", Toast.LENGTH_LONG)
.show()
}
}
)
}
O que essa função vai fazer é, basicamente, enviar um código de registro para o e-mail informado. Você pode tratar o erro e sucesso como preferir.
Após receber o código de registro, será necessário chamar outra função, que é a de registrar o usuário no app.
Criamos a função no nativo
if (call.method == REGISTER) {
ThingHomeSdk.getUserInstance().registerAccountWithEmail(
countryCode.toString(),
email.toString(),
password.toString(),
code.toString(),
object : IRegisterCallback {
override fun onSuccess(user: User?) {
Toast.makeText(context, "User: $user", Toast.LENGTH_LONG)
.show()
}
override fun onError(code: String?, error: String?) {
Toast.makeText(context, "Erro: $code - $error", Toast.LENGTH_LONG)
.show()
}
}
)
}
[!Important] As constantes apresentadas você pode cria-las no seu código. Como já foi apresentado, não vou criá-las novamente.
Para realizar o login, os argumentos passados são:
- Senha
- Código do país
Assim como nos demais, deve-se criar as constantes no Flutter e Kotlin. Não vou ficar recriando o código no Flutter, pois segue o mesmo padrão.
A função no Kotlin será algo assim:
if (call.method == AUTHENTICATE) {
ThingHomeSdk.getUserInstance().loginWithEmail(
countryCode.toString(),
email.toString(),
password.toString(),
object : ILoginCallback {
override fun onSuccess(user: User?) {
user?.let {
result.success(it.uid)
}
}
override fun onError(code: String?, error: String?) {
if (code != null && error != null) {
result.error(code, error, null)
};
}
}
)
}
Neste caso, caso tenha sucesso estou retornando o UID, que é o ID do usuário autenticado. E ai é interessante armazenar esse UID no Flutter, talvez cm o SharedPreferences, para mantê-lo logado futuramente.
No Flutter, após receber o UID, poderá navegar para a HOME do app. Caso tenha um erro, deve tratá-lo como for melhor.
Sempre que abrir o app, será necessário verificar se o usuário já está logado ou não. Para isso, podemos chamar a função
if (call.method == ALREADY_LOGGED) {
val isLogged = ThingHomeSdk.getUserInstance().isLogin
result.success(isLogged)
}
Ela vai verificar e retornar um Booleano. No Flutter podemos tratar isso e caso esteja, navegar para dentro do app.
A função para realizar logout do usuário é simples, pois o próprio SDK do tuya já nos fornece tudo.
if (call.method == LOGOUT) {
ThingHomeSdk.getUserInstance().logout(object : ILogoutCallback {
override fun onSuccess() {
Toast.makeText(context, "Logout success", Toast.LENGTH_SHORT).show()
result.success(true)
}
override fun onError(p0: String?, p1: String?) {
Toast.makeText(context, p1.toString(), Toast.LENGTH_SHORT).show()
result.error(p0.toString(), p1.toString(), null)
}
})
}
Caso tenha sucesso ou erro, novamente, trate no flutter como necessário.
Lembrando sempre de utilizar o result.success() com algum valor importante para ter um retorno sobre a função.
Atualmente estamos buscando os dispositivos para realizar pareamento via bluetooth. Para isso, é necessário solicitarmos a permissão do uso de bluetooth no app.
Dentro do AndroidManifest.xml adicione as permissões
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
[!IMPORTANT] Depois será necessário solicitar essas permissões para o usuário. Para fazer isso pode acessar a sessão de Solicitar permissões ao usuário.
Após termos adicionado as permissões no manifest, vamos ao código.
Seguindo o mesmo padrão, espero que já tenha criado as constantes necessárias, caso esteja fazendo dessa forma. O Código aqui já apresenta essas constantes, então não mostrarei o processo de criação novamente.
Temos então a função que faz a busca por dispositivos
fun bluetoothScan(callback: (ScanDeviceBean?) -> Unit) {
var deviceBean: ScanDeviceBean? = null
Toast.makeText(context, "Blueetooth Scan", Toast.LENGTH_SHORT).show()
val scanSetting = LeScanSetting.Builder()
.setTimeout(60000)
.addScanType(ScanType.SINGLE)
.build()
ThingHomeSdk.getBleOperator().startLeScan(
scanSetting
) { bean ->
callback(bean)
}
}
if (call.method == SEARCH_DEVICES) {
bluetoothScan { deviceBean ->
Log.i("scan", "bluetoothScan: ${deviceBean?.name.toString()}")
Toast.makeText(context, deviceBean?.data.toString(), Toast.LENGTH_SHORT).show()
ThingHomeSdk.getActivatorInstance().getActivatorDeviceInfo(
deviceBean?.productId,
deviceBean?.uuid,
deviceBean?.mac,
object: IThingDataCallback<ConfigProductInfoBean> {
override fun onSuccess(resConfigProductInfoBean: ConfigProductInfoBean?) {
Log.i("scan", "getDeviceInfo: ${resConfigProductInfoBean?.name.toString()}")
Toast.makeText(context, deviceBean?.data.toString(), Toast.LENGTH_SHORT).show()
if (resConfigProductInfoBean?.name != null) {
var deviceFound = arrayListOf(
resConfigProductInfoBean.name.toString(),
resConfigProductInfoBean.icon.toString()
)
result.success(deviceFound)
}
}
override fun onError(errorCode: String?, errorMessage: String?) {
Log.i("scan", "error getDeviceInfo: ${errorCode.toString()}")
Toast.makeText(context, errorMessage.toString(), Toast.LENGTH_SHORT).show()
}
})
}
}
- Observe que foi criado uma função fora do escopo ai da execução. Essa função é responsável por configurar o necessário para quando formos fazer a varredura de novos dispositivos. Ainda dentro dela, chamamos o startLeScan, passando as configurações necessárias. E passamos uma função callback que será executada quando iniciar a varredura.
- Essa função de callback é a função passada lá dentro do if abaixo.
- Chamamos a função criada que solicita um callback, o nosso callback será executado retornando algumas informações caso tenha sucesso ou erro.
- É no retorno de sucesso que vamos pegar as informações do dispositivo encontrado e retornar para o Flutter.
- Este Log.i... que tem no código é apenas uma forma útil de capturar algumas informações durante a execução. Neste caso, foi adicionado para no LOG enquanto o app é executado, eu consiga ter um retorno do nome do dispositivo.
Aqui é importante destacar os retornos. De acordo com a documentação do tuya, quando encontra o dispositivo será retornado várias informações e uma delas é o deviceType que vai nos trazer o tipo do dispositivo encontrado. É com esse tipo que vamos chamar a função correta para pareamento.
Temos os seguintes tipos
| 200 | config_type_single | Dispositivo Bluetooth |
|-------|----------------------------|
| 300 | config_type_single | Dispositivo Bluetooth |
| 301 | config_type_wifi | Dispositivo combinado Wi-Fi e Bluetooth |
| 304 | config_type_wifi | Dispositivo combinado Wi-Fi e Bluetooth que suporta emparelhamento por Bluetooth se uma conexão Wi-Fi não estiver disponível |
| 400 | config_type_single | Dispositivo Bluetooth |
| 401 | config_type_wifi | Dispositivo combinado Wi-Fi e Bluetooth |
| 404 | config_type_wifi | Dispositivo combinado Wi-Fi e Bluetooth que suporta emparelhamento por Bluetooth se uma conexão Wi-Fi não estiver disponível |
Caso queira encontrar, eis aqui o link: Varredura de dispositivos
Para para a busca por dispositivos, será necessário chamar uma outra função
if (call.method == STOP_SEARCH_DEVICES) {
ThingHomeSdk.getBleOperator().stopLeScan();
result.success(true)
}
A função vai parar a varredura e retornar um true para o Flutter, para que assim seja possível retornar algum feedback para o usuário.
Dessa vez utilizamos os argumentos solicitados juntamente cmo o codigo de registro. Novamente, você escolhe como tratar o erro e sucesso, mas recomendo caso tenha sucesso utilizar o RESULT para retornar algo para o Flutter e assim poder exibir um feedback para o usuário, ou navegar para a tela de login após registro.