Codelab do Firebase Cross Device

1. Introdução

Última atualização:14/03/2022

FlutterFire para comunicação entre dispositivos

Com o grande número de dispositivos de automação residencial, wearables e tecnologia de saúde pessoal entrando em operação, a comunicação entre dispositivos se torna uma parte cada vez mais importante da criação de aplicativos móveis. Configurar a comunicação entre dispositivos, como controlar um navegador em um app para smartphone ou controlar o que é reproduzido na TV pelo smartphone, é tradicionalmente mais complexo do que criar um app para dispositivos móveis normal .

O Realtime Database do Firebase oferece a API Presence , que permite aos usuários conferir o status on-line/off-line do dispositivo. Ela é usada com o serviço de instalações do Firebase para rastrear e conectar todos os dispositivos em que o mesmo usuário fez login. Você vai usar o Flutter para criar rapidamente aplicativos para várias plataformas e, em seguida, vai criar um protótipo entre dispositivos que toca música em um dispositivo e controla a música em outro.

O que você vai criar

Neste codelab, você vai criar um controle remoto simples para um player de música. Esse app vai:

  • Ter um player de música simples no Android, iOS e na Web, criado com o Flutter.
  • Permitir que os usuários façam login.
  • Conecte dispositivos quando o mesmo usuário estiver conectado em vários deles.
  • Permitir que os usuários controlem a reprodução de música em um dispositivo de outro.

7f0279938e1d3ab5.gif

O que você aprenderá

  • Como criar e executar um app de player de música do Flutter.
  • Como permitir que os usuários façam login com o Firebase Auth.
  • Como usar a API Presence do Firebase RTDB e o serviço de instalação do Firebase para conectar dispositivos.

O que é necessário

  • Um ambiente de desenvolvimento do Flutter. Siga as instruções no guia de instalação do Flutter para configurar.
  • É necessário ter o Flutter 2.10 ou uma versão mais recente. Se você tiver uma versão anterior, execute flutter upgrade.
  • Uma conta do Firebase.

2. Etapas da configuração

Acessar o código inicial

Criamos um app de reprodução de música no Flutter. O código inicial está localizado em um repositório Git. Para começar, na linha de comando, clone o repositório, acesse a pasta com o estado inicial e instale as dependências:

git clone https://github.com/FirebaseExtended/cross-device-controller.git

cd cross-device-controller/starter_code

flutter pub get

Criar o app

Você pode trabalhar com seu ambiente de desenvolvimento integrado favorito para criar o app ou usar a linha de comando.

No diretório do app, crie o app para a Web com o comando flutter run -d web-server.. Você vai ver o seguinte prompt.

lib/main.dart is being served at http://localhost:<port>

Acesse http://localhost:<port> para ver o player de música.

Se você conhece o emulador do Android ou o simulador do iOS, pode criar o app para essas plataformas e instalá-lo com o comando flutter run -d <device_name>.

O web app vai mostrar um player de música independente básico. Confira se os recursos do player estão funcionando corretamente. Este é um app player de música simples criado para este codelab. Ela só pode tocar uma música do Firebase, Better Together.

Configurar um emulador do Android ou um simulador do iOS

Se você já tiver um dispositivo Android ou iOS para desenvolvimento, pule esta etapa.

Para criar um emulador do Android, faça o download do Android Studio, que também oferece suporte ao desenvolvimento do Flutter, e siga as instruções em Criar e gerenciar dispositivos virtuais.

Para criar um simulador do iOS, você precisa de um ambiente Mac. Faça o download do XCode e siga as instruções em Visão geral do simulador > Usar o simulador > Abrir e fechar um simulador.

3. Configurar o Firebase

Criar um projeto do Firebase

  1. Faça login no console do Firebase usando sua Conta do Google.
  2. Clique no botão para criar um projeto e insira um nome (por exemplo, Firebase-Cross-Device-Codelab).
  3. Clique em Continuar.
  4. Se solicitado, leia e aceite os Termos do Firebase e clique em Continuar.
  5. (Opcional) Ative a assistência de IA no console do Firebase (chamada de "Gemini no Firebase").
  6. Neste codelab, você não precisa do Google Analytics. Portanto, desative a opção do Google Analytics.
  7. Clique em Criar projeto, aguarde o provisionamento e clique em Continuar.

Instalar o SDK do Firebase

De volta à linha de comando, no diretório do projeto, execute o seguinte comando para instalar o Firebase:

flutter pub add firebase_core

No arquivo pubspec.yaml, edite a versão do firebase_core para pelo menos 1.13.1 ou execute flutter upgrade

Inicializar o FlutterFire

  1. Se você não tiver a CLI do Firebase, instale-a executando curl -sL https://firebase.tools | bash.
  2. Faça login executando firebase login e seguindo as instruções.
  3. Instale a CLI do FlutterFire executando dart pub global activate flutterfire_cli.
  4. Configure a CLI do FlutterFire executando flutterfire configure.
  5. No prompt, escolha o projeto que você acabou de criar para este codelab, algo como Firebase-Cross-Device-Codelab.
  6. Selecione iOS, Android e Web quando for solicitado a escolher o suporte de configuração.
  7. Quando solicitado o ID do pacote da Apple, digite um domínio exclusivo ou insira com.example.appname, que é adequado para este codelab.

Depois de configurado, um arquivo firebase_options.dart será gerado com todas as opções necessárias para a inicialização.

No editor, adicione o seguinte código ao arquivo main.dart para inicializar o Flutter e o Firebase:

lib/main.dart

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
 
void main() async {
 WidgetsFlutterBinding.ensureInitialized();
 await Firebase.initializeApp(
   options: DefaultFirebaseOptions.currentPlatform,
 );
 runApp(const MyMusicBoxApp());
}

Compile o app com o comando:

flutter run

Você ainda não mudou nenhum elemento da interface, então a aparência e o comportamento do app não mudaram. Mas agora você tem um app do Firebase e pode começar a usar os produtos do Firebase, incluindo:

  • Firebase Authentication, que permite que os usuários façam login no app.
  • Firebase Realtime Database(RTDB): você vai usar a API Presence para rastrear o status on-line/off-line do dispositivo.
  • Com as regras de segurança do Firebase, é possível proteger o banco de dados.
  • Serviço de instalações do Firebase para identificar os dispositivos em que um único usuário fez login.

4. Adicionar o Firebase Auth

Ativar o login por e-mail para o Firebase Authentication

Para permitir que os usuários façam login no app da Web, use o método de login E-mail/Senha:

  1. No console do Firebase, expanda o menu Build no painel esquerdo.
  2. Clique em Autenticação, no botão Começar e na guia Método de login.
  3. Clique em E-mail/senha na lista Provedores de login, defina a chave Ativar como "Ativado" e clique em Salvar. 58e3e3e23c2f16a4.png

Configurar o Firebase Authentication no Flutter

Na linha de comando, execute os comandos a seguir para instalar os pacotes do Flutter necessários:

flutter pub add firebase_auth

flutter pub add provider

Com essa configuração, agora é possível criar o fluxo de login e logout. Como o estado de autenticação não deve mudar de tela para tela, você vai criar uma classe application_state.dart para acompanhar as mudanças de estado no nível do app, como fazer login e logout. Saiba mais sobre isso na documentação de gerenciamento de estado do Flutter.

Cole o seguinte no novo arquivo application_state.dart:

lib/src/application_state.dart

import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:flutter/material.dart';

import '../firebase_options.dart';
import 'authentication.dart';

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  Future<void> init() async {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
      } else {
        _loginState = ApplicationLoginState.loggedOut;
      }
      notifyListeners();
    });
  }

  ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
  ApplicationLoginState get loginState => _loginState;

  String? _email;
  String? get email => _email;

  void startLoginFlow() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> verifyEmail(
    String email,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      var methods =
          await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
      if (methods.contains('password')) {
        _loginState = ApplicationLoginState.password;
      } else {
        _loginState = ApplicationLoginState.register;
      }
      _email = email;
      notifyListeners();
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  Future<void> signInWithEmailAndPassword(
    String email,
    String password,
    void Function(FirebaseAuthException e) errorCallback,
  ) async {
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void cancelRegistration() {
    _loginState = ApplicationLoginState.emailAddress;
    notifyListeners();
  }

  Future<void> registerAccount(
      String email,
      String displayName,
      String password,
      void Function(FirebaseAuthException e) errorCallback) async {
    try {
      var credential = await FirebaseAuth.instance
          .createUserWithEmailAndPassword(email: email, password: password);
      await credential.user!.updateDisplayName(displayName);
    } on FirebaseAuthException catch (e) {
      errorCallback(e);
    }
  }

  void signOut() {
    FirebaseAuth.instance.signOut();
  }
}

Para garantir que o ApplicationState seja inicializado quando o app for iniciado, adicione uma etapa de inicialização ao main.dart:

lib/main.dart

import 'src/application_state.dart'; 
import 'package:provider/provider.dart';

void main() async {
  ... 
  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: (context, _) => const MyMusicBoxApp(),
  ));
}

Mais uma vez, a interface do aplicativo deve ter permanecido a mesma, mas agora você pode permitir que os usuários façam login e salvem os estados do app.

Criar um fluxo de login

Nesta etapa, você vai trabalhar no fluxo de login e logout. Este é o fluxo:

  1. Um usuário desconectado inicia o fluxo de login clicando no menu de contexto 71fcc1030a336423.png no lado direito da barra de apps.
  2. O fluxo de login vai aparecer em uma caixa de diálogo.
  3. Se o usuário nunca tiver feito login antes, ele vai precisar criar uma conta usando um endereço de e-mail válido e uma senha.
  4. Se o usuário já tiver feito login antes, ele vai precisar digitar a senha.
  5. Depois que o usuário fizer login, clicar no menu de contexto vai mostrar a opção Sair.

c295f6fa2e1d40f3.png

Adicionar um fluxo de login requer três etapas.

Primeiro, crie um widget AppBarMenuButton. Esse widget controla o pop-up do menu de contexto dependendo de um loginState do usuário. Adicionar as importações

lib/src/widgets.dart

import 'application_state.dart';
import 'package:provider/provider.dart';
import 'authentication.dart';

Adicione o seguinte código a widgets.dart.

lib/src/widgets.dart

class AppBarMenuButton extends StatelessWidget {
  const AppBarMenuButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
      builder: (context, appState, child) {
        if (appState.loginState == ApplicationLoginState.loggedIn) {
          return SignedInMenuButton(buildContext: context);
        }
        return SignInMenuButton(buildContext: context);
      },
    );
  }
}

class SignedInMenuButton extends StatelessWidget {
  const SignedInMenuButton({Key? key, required this.buildContext})
      : super(key: key);
  final BuildContext buildContext;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      onSelected: _handleSignedInMenu,
      color: Colors.deepPurple.shade300,
      itemBuilder: (context) => _getMenuItemBuilder(),
    );
  }

  List<PopupMenuEntry<String>> _getMenuItemBuilder() {
    return [
      const PopupMenuItem<String>(
        value: 'Sign out',
        child: Text(
          'Sign out',
          style: TextStyle(color: Colors.white),
        ),
      )
    ];
  }

  Future<void> _handleSignedInMenu(String value) async {
    switch (value) {
      case 'Sign out':
        Provider.of<ApplicationState>(buildContext, listen: false).signOut();
        break;
    }
  }
}

class SignInMenuButton extends StatelessWidget {
  const SignInMenuButton({Key? key, required this.buildContext})
      : super(key: key);
  final BuildContext buildContext;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      onSelected: _signIn,
      color: Colors.deepPurple.shade300,
      itemBuilder: (context) => _getMenuItemBuilder(context),
    );
  }

  Future<void> _signIn(String value) async {
    return showDialog<void>(
      context: buildContext,
      builder: (context) => const SignInDialog(),
    );
  }

  List<PopupMenuEntry<String>> _getMenuItemBuilder(BuildContext context) {
    return [
      const PopupMenuItem<String>(
        value: 'Sign in',
        child: Text(
          'Sign in',
          style: TextStyle(color: Colors.white),
        ),
      ),
    ];
  }
}

Em seguida, na mesma classe widgets.dart, crie o widget SignInDialog.

lib/src/widgets.dart

class SignInDialog extends AlertDialog {
  const SignInDialog({Key? key}) : super(key: key);

  @override
  AlertDialog build(BuildContext context) {
    return AlertDialog(
      content: Column(mainAxisSize: MainAxisSize.min, children: [
        Consumer<ApplicationState>(
          builder: (context, appState, _) => Authentication(
            email: appState.email,
            loginState: appState.loginState,
            startLoginFlow: appState.startLoginFlow,
            verifyEmail: appState.verifyEmail,
            signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
            cancelRegistration: appState.cancelRegistration,
            registerAccount: appState.registerAccount,
            signOut: appState.signOut,
          ),
        ),
      ]),
    );
  }
}

Em terceiro lugar, encontre o widget appBar em main.dart.. Adicione o AppBarMenuButton para mostrar a opção Fazer login ou Sair.

lib/main.dart

import 'src/widgets.dart';
appBar: AppBar(
  title: const Text('Music Box'),
  backgroundColor: Colors.deepPurple.shade400,
  actions: const <Widget>[
    AppBarMenuButton(),
  ],
),

Execute o comando flutter run para reiniciar o app com essas mudanças. O menu de contexto 71fcc1030a336423.png vai aparecer no lado direito da barra de apps. Clique nele para abrir uma caixa de diálogo de login.

Depois de fazer login com um endereço de e-mail e uma senha válidos, você vai encontrar a opção Sair no menu de contexto.

No console do Firebase, em Autenticação, você poderá ver o endereço de e-mail listado como um novo usuário.

888506c86a28a72c.png

Parabéns! Agora os usuários podem fazer login no app.

5. Adicionar conexão de banco de dados

Agora você pode passar para o registro de dispositivos usando a API Presence do Firebase.

Na linha de comando, execute os seguintes comandos para adicionar as dependências necessárias:

flutter pub add firebase_app_installations

flutter pub add firebase_database

Criar um banco de dados

No console do Firebase,

  1. Navegue até a seção Realtime Database do Console do Firebase. Clique em Criar banco de dados.
  2. Se for solicitado que você selecione um modo inicial para as regras de segurança, escolha Modo de teste por enquanto. O modo de teste cria regras de segurança que permitem todas as solicitações. Você vai adicionar regras de segurança mais tarde. É importante nunca entrar em produção com as regras de segurança ainda no modo de teste.)

O banco de dados está vazio por enquanto. Localize seu databaseURL em Configurações do projeto, na guia Geral. Role a tela para baixo até a seção Web apps.

1b6076f60a36263b.png

Adicione seu databaseURL ao arquivo firebase_options.dart:

lib/firebase_options.dart

 static const FirebaseOptions web = FirebaseOptions(
    apiKey: yourApiKey,
    ...
    databaseURL: 'https://<YOUR_DATABASE_URL>,
    ...
  );

Registrar dispositivos usando a API Presence do RTDB

Você quer registrar os dispositivos de um usuário quando eles aparecem on-line. Para isso, você vai aproveitar as instalações do Firebase e a API Presence do RTDB do Firebase para acompanhar uma lista de dispositivos on-line de um único usuário. O código a seguir vai ajudar a alcançar esse objetivo:

lib/src/application_state.dart

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_app_installations/firebase_app_installations.dart'; 

class ApplicationState extends ChangeNotifier {

  String? _deviceId;
  String? _uid;

  Future<void> init() async {
    ...
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _uid = user.uid;
        _addUserDevice();
      }
      ...
    });
  }

  Future<void> _addUserDevice() async {
    _uid = FirebaseAuth.instance.currentUser?.uid;

    String deviceType = _getDevicePlatform();
    // Create two objects which we will write to the
    // Realtime database when this device is offline or online
    var isOfflineForDatabase = {
      'type': deviceType,
      'state': 'offline',
      'last_changed': ServerValue.timestamp,
    };
    var isOnlineForDatabase = {
      'type': deviceType,
      'state': 'online',
      'last_changed': ServerValue.timestamp,
    };

    var devicesRef =
        FirebaseDatabase.instance.ref().child('/users/$_uid/devices');

    FirebaseInstallations.instance
        .getId()
        .then((id) => _deviceId = id)
        .then((_) {
      // Use the semi-persistent Firebase Installation Id to key devices
      var deviceStatusRef = devicesRef.child('$_deviceId');

      // RTDB Presence API
      FirebaseDatabase.instance
          .ref()
          .child('.info/connected')
          .onValue
          .listen((data) {
        if (data.snapshot.value == false) {
          return;
        }

        deviceStatusRef.onDisconnect().set(isOfflineForDatabase).then((_) {
          deviceStatusRef.set(isOnlineForDatabase);
        });
      });
    });
  }

  String _getDevicePlatform() {
    if (kIsWeb) {
      return 'Web';
    } else if (Platform.isIOS) {
      return 'iOS';
    } else if (Platform.isAndroid) {
      return 'Android';
    }
    return 'Unknown';
  }

De volta à linha de comando, crie e execute o app no dispositivo ou em um navegador com flutter run..

No app, faça login como um usuário. Faça login como o mesmo usuário em diferentes plataformas.

No console do Firebase, seus dispositivos vão aparecer em um ID de usuário no banco de dados.

5bef49cea3564248.png

6. Sincronizar o estado do dispositivo

Selecione um dispositivo principal

Para sincronizar estados entre dispositivos, designe um deles como líder ou controlador. O dispositivo principal vai determinar os estados nos dispositivos secundários.

Crie um método setLeadDevice em application_state.dart e rastreie este dispositivo com a chave active_device no RTDB:

lib/src/application_state.dart

  bool _isLeadDevice = false;
  String? leadDeviceType;

  Future<void> setLeadDevice() async {
    if (_uid != null && _deviceId != null) {
      var playerRef =
          FirebaseDatabase.instance.ref().child('/users/$_uid/active_device');
      await playerRef
          .update({'id': _deviceId, 'type': _getDevicePlatform()}).then((_) {
        _isLeadDevice = true;
      });
    }
  }

Para adicionar essa funcionalidade ao menu de contexto da barra de apps, crie um PopupMenuItem chamado Controller modificando o widget SignedInMenuButton. Nesse menu, os usuários podem definir o dispositivo principal.

lib/src/widgets.dart

class SignedInMenuButton extends StatelessWidget {
  const SignedInMenuButton({Key? key, required this.buildContext})
      : super(key: key);
  final BuildContext buildContext;

  List<PopupMenuEntry<String>> _getMenuItemBuilder() {
    return [
      const PopupMenuItem<String>(
        value: 'Sign out',
        child: Text(
          'Sign out',
          style: TextStyle(color: Colors.white),
        ),
      ),
      const PopupMenuItem<String>(
        value: 'Controller',
        child: Text(
          'Set as controller',
          style: TextStyle(color: Colors.white),
        ),
      )
    ];
  }

  void _handleSignedInMenu(String value) async {
    switch (value) {
      ...
      case 'Controller':
        Provider.of<ApplicationState>(buildContext, listen: false)
            .setLeadDevice();
    }
  }
}

Gravar o estado do dispositivo principal no banco de dados

Depois de definir um dispositivo principal, é possível sincronizar os estados dele com o RTDB usando o seguinte código. Adicione o código a seguir ao final de application_state.dart.. Isso vai começar a armazenar dois atributos: o estado do player (reproduzir ou pausar) e a posição do controle deslizante.

lib/src/application_state.dart

  Future<void> setLeadDeviceState(
      int playerState, double sliderPosition) async {
    if (_isLeadDevice && _uid != null && _deviceId != null) {
      var leadDeviceStateRef =
          FirebaseDatabase.instance.ref().child('/users/$_uid/active_device');
      try {
        var playerSnapshot = {
          'id': _deviceId,
          'state': playerState,
          'type': _getDevicePlatform(),
          'slider_position': sliderPosition
        };
        await leadDeviceStateRef.set(playerSnapshot);
      } catch (e) {
        throw Exception('updated playerState with error');
      }
    }
  }

Por fim, chame setActiveDeviceState sempre que o estado do jogador do controlador for atualizado. Faça as seguintes mudanças no arquivo player_widget.dart atual:

lib/player_widget.dart

import 'package:provider/provider.dart';
import 'application_state.dart';

 void _onSliderChangeHandler(v) {
    ...
    // update player state in RTDB if device is active
    Provider.of<ApplicationState>(context, listen: false)
        .setLeadDeviceState(_playerState.index, _sliderPosition);
 }

 Future<int> _pause() async {
    ...
    // update DB if device is active
    Provider.of<ApplicationState>(context, listen: false)
        .setLeadDeviceState(_playerState.index, _sliderPosition);
    return result;
  }

 Future<int> _play() async {
    var result = 0;

    // update DB if device is active
    Provider.of<ApplicationState>(context, listen: false)
        .setLeadDeviceState(PlayerState.PLAYING.index, _sliderPosition);

    if (_playerState == PlayerState.PAUSED) {
      result = await _audioPlayer.resume();
      return result;
    }
    ...
 }

 Future<int> _updatePositionAndSlider(Duration tempPosition) async {
    ...
    // update DB if device is active
    Provider.of<ApplicationState>(context, listen: false)
        .setLeadDeviceState(_playerState.index, _sliderPosition);
    return result;
  }

Ler o estado do dispositivo principal do banco de dados

Há duas partes para ler e usar o estado do dispositivo principal. Primeiro, configure um listener de banco de dados do estado do player principal em application_state. Esse listener informa aos dispositivos seguidores quando atualizar a tela por um callback. Você definiu uma interface OnLeadDeviceChangeCallback nesta etapa. Ela ainda não foi implementada. Você vai implementar essa interface em player_widget.dart na próxima etapa.

lib/src/application_state.dart

// Interface to be implemented by PlayerWidget
typedef OnLeadDeviceChangeCallback = void Function(
    Map<dynamic, dynamic> snapshot);

class ApplicationState extends ChangeNotifier {
  ...

  OnLeadDeviceChangeCallback? onLeadDeviceChangeCallback;

  Future<void> init() async {
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _uid = user.uid;
        _addUserDevice().then((_) => listenToLeadDeviceChange());
      }
      ...
    });
  }

  Future<void> listenToLeadDeviceChange() async {
    if (_uid != null) {
      var activeDeviceRef =
          FirebaseDatabase.instance.ref().child('/users/$_uid/active_device');
      activeDeviceRef.onValue.listen((event) {
        final activeDeviceState = event.snapshot.value as Map<dynamic, dynamic>;
        String activeDeviceKey = activeDeviceState['id'] as String;
        _isLeadDevice = _deviceId == activeDeviceKey;
        leadDeviceType = activeDeviceState['type'] as String;
        if (!_isLeadDevice) {
          onLeadDeviceChangeCallback?.call(activeDeviceState);
        }
        notifyListeners();
      });
    }
  }

Em segundo lugar, inicie o listener do banco de dados durante a inicialização do player em player_widget.dart. Transmita a função _updatePlayer para que o estado do jogador seguidor possa ser atualizado sempre que o valor do banco de dados mudar.

lib/player_widget.dart

class _PlayerWidgetState extends State<PlayerWidget> {

  @override
  void initState() {
    ...
    Provider.of<ApplicationState>(context, listen: false)
        .onLeadDeviceChangeCallback = updatePlayer;
  }

  void updatePlayer(Map<dynamic, dynamic> snapshot) {
    _updatePlayer(snapshot['state'], snapshot['slider_position']);
  }

  void _updatePlayer(dynamic state, dynamic sliderPosition) {
    if (state is int && sliderPosition is double) {
      try {
        _updateSlider(sliderPosition);
        final PlayerState newState = PlayerState.values[state];
        if (newState != _playerState) {
          switch (newState) {
            case PlayerState.PLAYING:
              _play();
              break;
            case PlayerState.PAUSED:
              _pause();
              break;
            case PlayerState.STOPPED:
            case PlayerState.COMPLETED:
              _stop();
              break;
          }
          _playerState = newState;
        }
      } catch (e) {
        if (kDebugMode) {
          print('sync player failed');
        }
      }
    }
  }

Agora você já pode testar o app:

  1. Na linha de comando, execute o app em emuladores e/ou em um navegador com: flutter run -d <device-name>
  2. Abra os apps em um navegador, em um simulador de iOS ou em um emulador de Android. Acesse o menu de contexto e escolha um app para ser o dispositivo principal. Você vai notar que os players dos dispositivos seguidores mudam conforme o dispositivo líder é atualizado.
  3. Agora mude o dispositivo principal, toque ou pause a música e observe os dispositivos secundários sendo atualizados de acordo.

Se os dispositivos secundários forem atualizados corretamente, você terá criado um controlador entre dispositivos. Falta apenas uma etapa crucial.

7. Atualizar regras de segurança

A menos que escrevamos regras de segurança melhores, alguém pode gravar um estado em um dispositivo que não é dele. Antes de terminar, atualize as regras de segurança do Realtime Database para garantir que apenas o usuário conectado a um dispositivo possa ler ou gravar nele. No Console do Firebase, navegue até o Realtime Database e clique na guia Regras. Cole as seguintes regras, permitindo que apenas o usuário conectado leia e grave os próprios estados do dispositivo:

{
  "rules": {
    "users": {
           "$uid": {
               ".read": "$uid === auth.uid",
               ".write": "$uid === auth.uid"
           }
    },
  }
}

8. Parabéns!

bcd986f7106d892b.gif

Parabéns! Você criou um controle remoto entre dispositivos usando o Flutter.

Créditos

Better Together, uma música do Firebase

  • Música de Ryan Vernon
  • Letra e capa do álbum de Marissa Christy
  • Voz de JP Gomez

9. Bônus

Como um desafio extra, use o Flutter FutureBuilder para adicionar o tipo de dispositivo principal atual à interface de maneira assíncrona. Se você precisar de ajuda, ela será implementada na pasta que contém o estado final do codelab.

Documentos de referência e próximas etapas