Codelab entre dispositivos do Firebase

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. Configurar a comunicação entre dispositivos, como controlar um navegador a partir de um aplicativo para celular ou controlar o que é reproduzido na TV a partir do telefone, é tradicionalmente mais complexo do que criar um aplicativo móvel normal .

o Realtime Database do Firebase fornece a API Presence , que permite aos usuários ver o status on-line/off-line dos dispositivos. você a usará 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 aplicativos rapidamente para várias plataformas e, em seguida, vai construir um protótipo para vários dispositivos que toca música em um dispositivo e controla a música em outro.

O que você vai criar

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

  • Crie um player de música simples no Android, iOS e Web com o Flutter.
  • Permita 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 RTDB Presence do Firebase 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 fazer a configuração.
  • É 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, 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 Web com o comando flutter run -d web-server.. O prompt a seguir vai aparecer.

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

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

Se você já conhece o Android Emulator ou o simulador de 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 simples de player de música projetado para este codelab. Ele 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ê 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

Criar um projeto do Firebase

Abra um navegador para 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 seu projeto 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 você não vai usá-lo para este app.

Não é necessário fazer o download dos arquivos mencionados ou mudar os arquivos build.gradle. Elas serão configuradas ao inicializar o FlutterFire.

Instalar o SDK do Firebase

De volta 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 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 instalada, execute curl -sL https://firebase.tools | bash para instalá-la.
  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 o ID do pacote da Apple for solicitado, digite um domínio exclusivo ou com.example.appname, o que não é um problema 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

Como você ainda não mudou nenhum elemento da interface, 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 de presença para rastrear o status on-line/off-line do dispositivo
  • As regras de segurança do Firebase vão permitir que você proteja o banco de dados.
  • O 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 comandos abaixo 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 muda de uma tela para a outra, crie 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 o 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(),
  ));
}

A interface do aplicativo deve 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ê trabalhará no fluxo de login e logout. Confira como o fluxo vai ficar:

  1. Um usuário desconectado iniciará o fluxo de login clicando no menu de contexto 71fcc1030a336423.pngno lado direito da barra de apps.
  2. O fluxo de login será mostrado em uma caixa de diálogo.
  3. Se o usuário nunca tiver feito login antes, ele receberá uma solicitação para 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 receberá uma solicitação para 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 menu de contexto pop-up, 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),
        ),
      ),
    ];
  }
}

Depois, 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,
          ),
        ),
      ]),
    );
  }
}

Depois, encontre o widget da barra de apps 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ê já pode prosseguir para o registro de dispositivos usando a API Firebase Presence.

Na linha de comando, execute os comandos abaixo 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 a passagem de 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 por enquanto. Localize o databaseURL em Configurações do projeto, na guia Geral. Role 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, use as instalações do Firebase e a API RTDB Presence do Firebase para monitorar 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, seus dispositivos vão aparecer com 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 o controlador. O dispositivo principal vai determinar os estados nos dispositivos secundários.

Crie um método setLeadDevice no 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, é necessário chamar setActiveDeviceState sempre que o estado do player do controle for atualizado. Faça as seguintes alterações 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, configure um listener de banco de dados para o estado do player principal em application_state. Esse listener informará aos dispositivos seguidores quando atualizar a tela por meio de 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 seguida, 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 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 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 é dele. Portanto, antes de terminar, atualize as regras de segurança do Realtime Database para garantir que os únicos usuários que podem ler ou gravar em um dispositivo sejam os que estão conectados a ele. No Console do Firebase, navegue até o Realtime Database e, em seguida, até a guia Regras. Cole as seguintes regras que permitem que apenas usuários conectados leiam e gravem 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

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

9. Bônus

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

Documentos de referência e próximas etapas