Codelab do Firebase Cross Device

1. Introdução

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

FlutterFire para comunicação entre dispositivos

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

O Firebase Realtime Database oferece a API Presence , que permite que os usuários vejam o status on-line/off-line do dispositivo. Você vai usar essa API 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, criar um protótipo multiplataforma 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:

  • Tenha um player de música simples no Android, iOS e na Web, criado com o Flutter.
  • Permitir que os usuários façam login.
  • Conectar dispositivos quando o mesmo usuário estiver conectado em vários dispositivos.
  • Permitir que os usuários controlem a reprodução de música em um dispositivo usando 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ária uma versão mínima do Flutter 2.10 ou 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 player 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, mova para 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 Web com o comando flutter run -d web-server.. Você vai ver a seguinte solicitação.

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

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

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

O app da Web deve mostrar um player de música independente básico. Confira se os recursos do player estão funcionando como esperado. Este é um app de player de música simples desenvolvido para este codelab. Ele só pode tocar uma música do Firebase, Better Together.

Configurar um emulador Android ou um simulador iOS

Se você já tem 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ê vai precisar 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. Como configurar o Firebase

Crieum projeto do Firebase

Abra um navegador em http://console.firebase.google.com/.

  1. Faça login no Firebase.
  2. No console do Firebase, clique em Adicionar projeto (ou Criar um projeto) e nomeie o projeto do Firebase como Firebase-Cross-Device-Codelab.
  3. Clique nas opções de criação do projeto. Se for solicitado, aceite os termos do Firebase. Ignore a configuração do Google Analytics, porque ele não vai ser usado.

Não é necessário fazer o download dos arquivos mencionados ou mudar os arquivos build.gradle. Você vai configurá-los ao inicializar o FlutterFire.

Instalar o SDK do Firebase

Na 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 de 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 instalada, execute curl -sL https://firebase.tools | bash para fazer a instalação.
  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 que você escolha o suporte à configuração.
  7. Quando solicitado o ID de pacote da Apple, digite um domínio exclusivo ou insira com.example.appname, que é adequado para os fins deste 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());
}

Compilar 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): use a API de presença para acompanhar o status on-line/off-line do dispositivo.
  • As regras de segurança do Firebase permitem 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, abra o menu Build no painel à esquerda.
  2. Clique em Autenticação e no botão Começar, depois na guia Método de login.
  3. Clique em E-mail/senha na lista Provedores de login, ative a chave Ativar e clique em Salvar. 58e3e3e23c2f16a4.png

Configurar o Firebase Authentication no Flutter

Na linha de comando, execute os seguintes comandos para instalar os pacotes 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 pode 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 sair. Saiba mais sobre isso na documentação 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 ApplicationState seja inicializado quando o app for iniciado, adicione uma etapa de inicialização a 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(),
  ));
}

Novamente, a interface do aplicativo precisa permanecer 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. Confira como o fluxo vai ficar:

  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 fez login antes, ele vai precisar criar uma conta usando um endereço de e-mail e uma senha válidos.
  4. Se o usuário já tiver feito login, 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

A adição do fluxo de login requer três etapas.

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

lib/src/widgets.dart

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

Anexe 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 segundo lugar, 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,
          ),
        ),
      ]),
    );
  }
}

Terceiro, 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. Ao clicar nele, você vai acessar 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ê vai encontrar o endereço de e-mail listado como um novo usuário.

888506c86a28a72c.png

Parabéns! Os usuários já podem fazer login no app.

5. Adicionar conexão de banco de dados

Agora você está pronto para passar para o registro de dispositivos usando a API Firebase Presence.

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 você precisar selecionar um modo inicial para suas 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 as regras de segurança mais tarde. É importante nunca ir para a produção com as regras de segurança ainda no modo de teste.

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

1b6076f60a36263b.png

Adicione o 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 RTDB Presence

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 Firebase RTDB 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';
  }

Na 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. Não se esqueça de fazer login como o mesmo usuário em plataformas diferentes.

No Console do Firebase, os dispositivos aparecem em um ID de usuário no banco de dados.

5bef49cea3564248.png

6. Sincronizar o estado do dispositivo

Selecionar um dispositivo principal

Para sincronizar estados entre dispositivos, designe um dispositivo 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 esse 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 uma PopupMenuItem chamada Controller modificando o widget SignedInMenuButton. Esse menu permite que os usuários definam 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, você pode sincronizar os estados dele com o RTDB usando o código abaixo. Anexe o código abaixo ao final de application_state.dart.. Isso vai começar a armazenar dois atributos: o estado do player (reprodução ou pausa) 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, você precisa chamar setActiveDeviceState sempre que o estado do jogador do controlador for atualizado. Faça as seguintes mudanças no arquivo player_widget.dart:

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, você quer configurar um listener de banco de dados do estado do lead player em application_state. Esse listener vai informar aos dispositivos seguidores quando atualizar a tela por um callback. Você definiu uma interface OnLeadDeviceChangeCallback nesta etapa. Ela ainda não está 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 player de seguidores 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 do iOS ou em um emulador do Android. Acesse o menu de contexto e escolha um app para ser o dispositivo líder. Você vai notar que os players dos dispositivos seguidores mudam conforme o dispositivo líder é atualizado.
  3. Agora mude o dispositivo líder, toque ou pause a música e observe os dispositivos seguidores sendo atualizados.

Se os dispositivos seguidores forem atualizados corretamente, você terá criado um controlador entre dispositivos. Só falta uma etapa importante.

7. Atualizar regras de segurança

A menos que criemos regras de segurança melhores, alguém pode gravar um estado em um dispositivo que não é de sua propriedade. Antes de terminar, atualize as regras de segurança do Realtime Database para garantir que apenas o usuário conectado possa ler ou gravar em um dispositivo. No Console do Firebase, acesse o Realtime Database e a guia Regras. Cole as seguintes regras para permitir 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 para vários dispositivos usando o Flutter.

Créditos

Better Together, uma música do Firebase

  • Music by Ryan Vernon
  • Letra e capa do álbum de Marissa Christy
  • Voz de JP Gomez

9. Bônus

Como desafio extra, considere usar o Flutter FutureBuilder para adicionar o tipo de dispositivo principal atual à interface de forma assíncrona. Se você precisar de ajuda, ela está implementada na pasta que contém o estado concluído do codelab.

Documentos de referência e próximas etapas