Codelab do Firebase para vários dispositivos

1. Introdução

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

FlutterFire para comunicação entre dispositivos

À medida que testemunhamos um grande número de dispositivos de automação residencial, dispositivos vestíveis e de tecnologia de saúde pessoal ficando on-line, a comunicação entre dispositivos torna-se uma parte cada vez mais importante da construção de aplicativos móveis. Configurar a comunicação entre dispositivos, como controlar um navegador a partir de um aplicativo de telefone 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 online/offline de seus dispositivos; você o usará com o Firebase Installations Service para rastrear e conectar todos os dispositivos onde o mesmo usuário fez login. Você usará o Flutter para criar rapidamente aplicativos para várias plataformas e, em seguida, criará um protótipo entre dispositivos que reproduza música em um dispositivo e controla a música em outro!

O que você construirá

Neste codelab, você criará um controle remoto simples para um player de música. Seu aplicativo irá:

  • Tenha um reprodutor de música simples para Android, iOS e web, desenvolvido com Flutter.
  • Permitir que os usuários façam login.
  • Conecte 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 a partir de outro dispositivo.

7f0279938e1d3ab5.gif

O que você aprenderá

  • Como construir e executar um aplicativo reprodutor de música Flutter.
  • Como permitir que os usuários façam login com o Firebase Auth.
  • Como usar a API Firebase RTDB Presence e o Firebase Installation Service para conectar dispositivos.

O que você precisará

  • Um ambiente de desenvolvimento Flutter. Siga as instruções no guia de instalação do Flutter para configurá-lo.
  • É necessária uma versão mínima do Flutter 2.10 ou superior. Se você tiver uma versão inferior, execute flutter upgrade.
  • Uma conta do Firebase.

2. Preparando-se

Obtenha o código inicial

Criamos um aplicativo reprodutor 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, vá 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

Crie o aplicativo

Você pode trabalhar com seu IDE favorito para criar o aplicativo ou usar a linha de comando.

No diretório do seu aplicativo, crie o aplicativo para web com o comando flutter run -d web-server. Você deverá conseguir ver o seguinte prompt.

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

Visite http://localhost:<port> para ver o reprodutor de música.

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

O aplicativo da web deve mostrar um reprodutor de música autônomo básico. Certifique-se de que os recursos do player estejam funcionando conforme esperado. Este é um app reprodutor de música simples desenvolvido para este codelab. Ele só pode reproduzir uma música do Firebase, Better Together .

Configure um emulador Android ou um simulador iOS

Se você já possui um dispositivo Android ou iOS para desenvolvimento, pode pular esta etapa.

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

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

3. Configurando o Firebase

Crie um projeto 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 seu projeto do Firebase como Firebase-Cross-Device-Codelab .
  3. Clique nas opções de criação do projeto. Aceite os termos do Firebase, se solicitado. Ignore a configuração do Google Analytics porque você não usará o Analytics para este aplicativo.

Você não precisa baixar os arquivos mencionados ou alterar os arquivos build.gradle. Você irá configurá-los ao inicializar o FlutterFire.

Instale 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 de firebase_core para que seja pelo menos 1.13.1 ou execute flutter upgrade

Inicializar FlutterFire

  1. Se você não tiver a CLI do Firebase instalada, poderá instalá-la 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 o ID do pacote Apple for solicitado, digite um domínio exclusivo ou insira com.example.appname , o que é adequado para a finalidade deste codelab.

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

Em seu 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 aplicativo com o comando:

flutter run

Você ainda não alterou nenhum elemento da IU, portanto a aparência e o comportamento do aplicativo não foram alterados. Mas agora você tem um aplicativo Firebase e pode começar a usar os produtos Firebase, incluindo:

  • Firebase Authentication , que permite que seus usuários façam login no seu aplicativo.
  • Banco de dados em tempo real Firebase (RTDB) ; você usará a API de presença para rastrear o status on-line/off-line do dispositivo
  • As regras de segurança do Firebase permitirão proteger o banco de dados.
  • Serviço Firebase Installations para identificar os dispositivos nos quais um único usuário fez login.

4. Adicionar autenticação do Firebase

Ativar login por e-mail para Firebase Authentication

Para permitir que os usuários façam login no aplicativo Web, você usará o método de login por e-mail/senha :

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

Configurar a autenticação do Firebase 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 esta configuração, agora você pode criar o fluxo de entrada e saída. Como o estado de autenticação não deve mudar de tela para tela, você criará uma classe application_state.dart para acompanhar as alterações de estado no nível do aplicativo, como 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 ApplicationState seja inicializado quando o aplicativo for iniciado, você adicionará 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 IU do aplicativo deveria ter permanecido a mesma, mas agora você pode permitir que os usuários façam login e salvem os estados do aplicativo.

Crie um fluxo de login

Nesta etapa, você trabalhará no fluxo de entrada e saída. Aqui está como será o fluxo:

  1. Um usuário desconectado iniciará o fluxo de login clicando no menu de contexto 71fcc1030a336423.png no lado direito da barra de aplicativos.
  2. O fluxo de login será exibido em uma caixa de diálogo.
  3. Se o usuário nunca tiver feito login antes, ele será solicitado a 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 será solicitado a inserir sua senha.
  5. Depois que o usuário estiver conectado, clicar no menu de contexto exibirá a opção Sair .

c295f6fa2e1d40f3.png

Adicionar fluxo de login requer três etapas.

Primeiro de tudo, crie um widget AppBarMenuButton . Este widget controlará o pop-up do menu de contexto dependendo do loginState do usuário. Adicione 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),
        ),
      ),
    ];
  }
}

Segundo, 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 existente em main.dart. Adicione o AppBarMenuButton para exibir a opção Entrar 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 aplicativo com essas alterações. Você deve conseguir ver o menu de contexto 71fcc1030a336423.png no lado direito da barra de aplicativos. Clicar nele o levará a uma caixa de diálogo de login.

Depois de fazer login com um endereço de e-mail válido e uma senha, você verá 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! Os usuários agora podem entrar no aplicativo!

5. Adicione conexão com o banco de dados

Agora você está pronto para prosseguir com o registro do dispositivo 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

Crie 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 a selecionar um modo inicial para suas regras de segurança, selecione Modo de teste por enquanto**.** (O modo de teste cria regras de segurança que permitem a passagem de todas as solicitações. Você adicionará regras de segurança mais tarde. É importante nunca ir para a produção com suas regras de segurança ainda estão no modo de teste.)

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

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>,
    ...
  );

Registre dispositivos usando a API RTDB Presence

Você deseja registrar os dispositivos de um usuário quando eles aparecerem online. Para fazer isso, você aproveitará as vantagens do Firebase Installations e da Firebase RTDB Presence API para acompanhar uma lista de dispositivos on-line de um único usuário. O código a seguir ajudará a atingir 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 aplicativo em seu dispositivo ou em um navegador com flutter run.

No seu aplicativo, faça login como usuário. Lembre-se de fazer login como o mesmo usuário em plataformas diferentes.

No console do Firebase , você verá seus dispositivos aparecendo com um ID de usuário em seu banco de dados.

5bef49cea3564248.png

6. Sincronize o estado do dispositivo

Selecione um dispositivo de chumbo

Para sincronizar estados entre dispositivos, designe um dispositivo como líder ou controlador. O dispositivo líder ditará os estados dos dispositivos seguidores.

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 aplicativos, crie um PopupMenuItem chamado Controller modificando o widget SignedInMenuButton . Este menu permitirá que os usuários configurem 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();
    }
  }
}

Grave o estado do dispositivo líder no banco de dados

Depois de definir um dispositivo líder, você pode sincronizar os estados do dispositivo líder com o RTDB com o código a seguir. Anexe o código a seguir ao final de application_state.dart. Isso 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');
      }
    }
  }

E, finalmente, você precisa chamar setActiveDeviceState sempre que o estado do player do controlador for atualizado. Faça as seguintes alterações no arquivo player_widget.dart existente:

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;
  }

Leia o estado do dispositivo líder no banco de dados

Existem duas partes para ler e usar o estado do dispositivo condutor. Primeiro, você deseja configurar um ouvinte de banco de dados do estado do player principal em application_state . Este ouvinte informará aos dispositivos seguidores quando atualizar a tela por meio de um retorno de chamada. Observe que você definiu uma interface OnLeadDeviceChangeCallback nesta etapa. Ainda não foi implementado; você 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();
      });
    }
  }

Segundo, inicie o ouvinte do banco de dados durante a inicialização do player em player_widget.dart . Passe a função _updatePlayer para que o estado do player seguidor possa ser atualizado sempre que o valor do banco de dados for alterado.

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ê está pronto para testar o aplicativo:

  1. Na linha de comando, execute o aplicativo em emuladores e/ou em um navegador com: flutter run -d <device-name>
  2. Abra os aplicativos em um navegador, em um simulador de iOS ou em um emulador de Android. Vá para o menu de contexto e escolha um aplicativo para ser o dispositivo líder. Você poderá ver os jogadores dos dispositivos seguidores mudarem à medida que o dispositivo líder for atualizado.
  3. Agora mude o dispositivo líder, reproduza ou pause a música e observe os dispositivos seguidores sendo atualizados de acordo.

Se os dispositivos seguidores forem atualizados corretamente, você conseguiu criar um controlador entre dispositivos. Falta apenas um passo crucial.

7. Atualizar regras de segurança

A menos que escrevamos regras de segurança melhores, alguém poderá escrever um estado em um dispositivo que não seja de sua propriedade! 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 aqueles que estão conectados a esse dispositivo. No Firebase Console, navegue até o Realtime Database e depois até a guia Regras . Cole as seguintes regras, permitindo que apenas usuários conectados leiam e gravem seus próprios estados de dispositivo:

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

8. Parabéns!

bcd986f7106d892b.gif

Parabéns, você construiu com sucesso um controle remoto entre dispositivos usando 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 adicional, considere usar o Flutter FutureBuilder para adicionar o tipo de dispositivo líder atual à IU de forma assíncrona. Se precisar de ajuda, ela será implementada na pasta que contém o estado final do codelab.

Documentos de referência e próximas etapas