Pārlūkot izejas kodu

feat(mobile): scrolling and fetch improvements

    * feat: hold scroll position in artists list when selecting albums
    * refactor: moved fetch logic into one place and socket functions into separate class
Fela Maslen 4 gadi atpakaļ
vecāks
revīzija
a456d9c165

+ 7 - 27
gmus-mobile/lib/components/albums.dart

@@ -8,21 +8,6 @@ import '../utils/url.dart';
 
 import './spinner.dart';
 
-class Albums extends StatefulWidget {
-  final String apiUrl;
-  final String artist;
-  final void Function(String, String) onSelect;
-
-  Albums({
-    @required this.apiUrl,
-    @required this.artist,
-    @required this.onSelect,
-  });
-
-  @override
-  _AlbumsWidgetState createState() => _AlbumsWidgetState(apiUrl: this.apiUrl, artist: this.artist, onSelect: this.onSelect);
-}
-
 Future<List<String>> fetchAlbums(String apiUrl, String artist) async {
   final response = await http.get(formattedUrl(apiUrl, '/albums', {
     'artist': artist,
@@ -37,27 +22,22 @@ Future<List<String>> fetchAlbums(String apiUrl, String artist) async {
 
 const allAlbums = 'All albums';
 
-class _AlbumsWidgetState extends State<Albums> {
-  final String apiUrl;
+class Albums extends StatelessWidget {
   final String artist;
-  Future<List<String>> albums;
-
+  final Future<List<String>> albums;
   final void Function(String, String) onSelect;
 
-  _AlbumsWidgetState({
-    @required this.apiUrl,
+  Albums({
     @required this.artist,
+    @required this.albums,
     @required this.onSelect,
   });
 
-  @override
-  void initState() {
-    super.initState();
-    albums = fetchAlbums(this.apiUrl, this.artist);
-  }
-
   @override
   Widget build(BuildContext context) {
+    if (artist == null || albums == null) {
+      return Container();
+    }
     return FutureBuilder<List<String>>(
       future: albums,
       builder: (context, snapshot) {

+ 1 - 2
gmus-mobile/lib/components/apiurl.dart

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 
 import '../controller.dart';
-import '../socket.dart' as socket;
 
 class _SetApiUrl extends StatefulWidget {
   @override
@@ -33,7 +32,7 @@ class _SetApiUrlState extends State<_SetApiUrl> {
         TextButton(
           child: Text('Set'),
           onPressed: () {
-            socket.setApiUrl(controller, this.apiUrl);
+            controller.setApiUrl(apiUrl);
             Navigator.pop(context);
           },
         ),

+ 22 - 60
gmus-mobile/lib/components/artists.dart

@@ -7,21 +7,7 @@ import '../utils/url.dart';
 
 import './spinner.dart';
 
-class Artists extends StatefulWidget {
-  final String apiUrl;
-  final void Function(String) onSelect;
-
-  Artists({
-    @required this.apiUrl,
-    @required this.onSelect,
-  });
-
-  @override
-  _ArtistsWidgetState createState() => _ArtistsWidgetState(apiUrl: this.apiUrl, onSelect: this.onSelect);
-}
-
 Future<List<String>> fetchArtists(String apiUrl) async {
-
   final response = await http.get(formattedUrl(apiUrl, '/artists'));
 
   if (response.statusCode == 200) {
@@ -31,59 +17,35 @@ Future<List<String>> fetchArtists(String apiUrl) async {
   }
 }
 
-class _ArtistsWidgetState extends State<Artists> {
-  String apiUrl;
-  Future<List<String>> artists;
-
+class Artist extends StatelessWidget {
+  final String artist;
   final void Function(String) onSelect;
-
-  _ArtistsWidgetState({
-    @required this.apiUrl,
+  Artist({
+    @required this.artist,
     @required this.onSelect,
   });
 
-  @override
-  void initState() {
-    super.initState();
-    artists = fetchArtists(this.apiUrl);
-  }
-
   @override
   Widget build(BuildContext context) {
-    return FutureBuilder<List<String>>(
-      future: artists,
-      builder: (context, snapshot) {
-        if (snapshot.hasData) {
-          return ListView(
-            padding: EdgeInsets.only(left: 8, right: 8),
-            children: snapshot.data.map((artist) => InkWell(
-              onTap: () {
-                onSelect(artist);
-              },
-              child: SizedBox(
-                height: 40,
-                width: MediaQuery.of(context).size.width,
-                child: Container(
-                  child: Align(
-                    alignment: Alignment.centerLeft,
-                    child: Text(
-                      artist.length == 0 ? 'Unknown artist' : artist,
-                      style: TextStyle(
-                        fontSize: 18,
-                      ),
-                    ),
-                  ),
-                ),
-              ),
-            )).toList(),
-          );
-        }
-        if (snapshot.hasError) {
-          return Text("${snapshot.error}");
-        }
-
-        return CenterSpinner();
+    return InkWell(
+      onTap: () {
+        onSelect(artist);
       },
+      child: SizedBox(
+        height: 40,
+        width: MediaQuery.of(context).size.width,
+        child: Container(
+          child: Align(
+            alignment: Alignment.centerLeft,
+            child: Text(
+              artist.length == 0 ? 'Unknown artist' : artist,
+              style: TextStyle(
+                fontSize: 18,
+              ),
+            ),
+          ),
+        ),
+      ),
     );
   }
 }

+ 66 - 27
gmus-mobile/lib/components/browser.dart

@@ -1,5 +1,8 @@
 import 'package:flutter/widgets.dart';
 import 'package:get/get.dart';
+import 'package:gmus/components/spinner.dart';
+import 'package:gmus/components/statefullist.dart';
+import 'package:gmus/types/song.dart';
 
 import '../controller.dart';
 
@@ -12,13 +15,19 @@ class Browser extends StatefulWidget {
   _BrowserWidgetState createState() => _BrowserWidgetState();
 }
 
-class _BrowserWidgetState extends State<Browser> {
+class _BrowserWidgetState extends State<Browser> with SingleTickerProviderStateMixin {
   final Controller controller = Get.find();
 
-  PageController pageController = PageController();
+  PageController _pageController;
+  Widget _artistList;
+  RxBool _artistsLoading = false.obs;
+
+  RxList<String> _artists = <String>[].obs;
+  Rx<Future<List<String>>> _albums = Future.value(<String>[]).obs;
+  Rx<Future<List<Song>>> _songs = Future.value(<Song>[]).obs;
 
   void _jumpToPage(int page) {
-    pageController.animateToPage(page, duration: Duration(milliseconds: 500), curve: Curves.ease);
+    _pageController.animateToPage(page, duration: Duration(milliseconds: 500), curve: Curves.ease);
   }
   void _jumpToAlbums() {
     this._jumpToPage(1);
@@ -30,42 +39,72 @@ class _BrowserWidgetState extends State<Browser> {
   RxString selectedArtist;
   RxString selectedAlbum;
 
-  void onSelectArtist(String artist) {
+  void _onSelectArtist(String artist) {
     this.selectedArtist = artist.obs;
+    _albums.value = fetchAlbums(controller.apiUrl.value, this.selectedArtist.value);
     this._jumpToAlbums();
   }
 
-  void onSelectAlbum(String artist, String album) {
+  void _onSelectAlbum(String artist, String album) {
     this.selectedAlbum = album.obs;
+    _songs.value = fetchSongs(controller.apiUrl.value, this.selectedArtist.value);
     this._jumpToSongs();
   }
 
-  void onSelectSong(int songId) {
+  void _onSelectSong(int songId) {
     controller.playSong(songId);
   }
 
+  Future<void> _loadArtists() async {
+    _artistsLoading.value = true;
+    _artists.assignAll(await fetchArtists(controller.apiUrl.value));
+    _artistsLoading.value = false;
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _pageController = new PageController();
+    _loadArtists();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _pageController.dispose();
+  }
+
+  Widget _buildArtist(BuildContext context, int index) {
+    return Artist(artist: _artists[index], onSelect: _onSelectArtist);
+  }
+
   @override
   Widget build(BuildContext context) {
-    return PageView(
-      controller: pageController,
-      children: <Widget>[
-        Obx(() => Artists(
-          apiUrl: controller.apiUrl.value,
-          onSelect: this.onSelectArtist,
-        )),
-        Obx(() => Albums(
-          apiUrl: controller.apiUrl.value,
-          artist: this.selectedArtist.value,
-          onSelect: this.onSelectAlbum,
-        )),
-        Obx(() => Songs(
-          apiUrl: controller.apiUrl.value,
-          artist: this.selectedArtist.value,
-          album: this.selectedAlbum.value,
-          onSelect: this.onSelectSong,
-        )),
-      ],
-      pageSnapping: true,
-    );
+    return Obx(() {
+      if (_artistsLoading.isTrue) {
+        return CenterSpinner();
+      }
+      if (_artistList == null) {
+        _artistList = new StatefulListView(_artists.length, _buildArtist);
+      }
+      return PageView(
+        controller: _pageController,
+        children: <Widget>[
+          _artistList,
+          Obx(() => Albums(
+            artist: this.selectedArtist?.value,
+            albums: this._albums.value,
+            onSelect: this._onSelectAlbum,
+          )),
+          Obx(() => Songs(
+            artist: this.selectedArtist?.value,
+            album: this.selectedAlbum?.value,
+            songs: this._songs.value,
+            onSelect: this._onSelectSong,
+          )),
+        ],
+        pageSnapping: true,
+      );
+    });
   }
 }

+ 4 - 1
gmus-mobile/lib/components/content.dart

@@ -19,7 +19,10 @@ class Content extends StatelessWidget {
       if (!loggedIn) {
         return Identify();
       }
-      if (!controller.connected.isTrue) {
+      if (controller.socket.error.value.length != 0) {
+        return Center(child: Text(controller.socket.error.value));
+      }
+      if (!controller.socket.connected.value) {
         return CenterSpinner();
       }
 

+ 2 - 2
gmus-mobile/lib/components/identify.dart

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 
 import '../controller.dart';
-import '../socket.dart' as socket;
 
 class Identify extends StatelessWidget {
   final Controller controller = Get.find();
@@ -10,6 +9,7 @@ class Identify extends StatelessWidget {
   Widget build(BuildContext context) {
     return Column(
       children: [
+        Text("Current name: ${controller.name.value}"),
         TextField(
             onChanged: controller.setName,
             decoration: InputDecoration(
@@ -20,7 +20,7 @@ class Identify extends StatelessWidget {
         TextButton(
             child: Text('Connect'),
             onPressed: () {
-              socket.connect(controller);
+              controller.connect();
             },
         ),
       ],

+ 4 - 5
gmus-mobile/lib/components/player.dart

@@ -1,6 +1,5 @@
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
-import 'package:gmus/socket.dart';
 
 import '../controller.dart';
 
@@ -21,13 +20,13 @@ class GmusPlayer extends StatelessWidget {
           ),
           TextButton(
             child: Text('Disconnect'),
-            onPressed: () => disconnect(this.controller),
+            onPressed: controller.disconnect,
           ),
           TextButton(
             child: Text('Reconnect'),
-            onPressed: () {
-              disconnect(this.controller);
-              connect(this.controller);
+            onPressed: () async {
+              controller.disconnect();
+              await controller.connect();
             },
           ),
         ],

+ 9 - 36
gmus-mobile/lib/components/songs.dart

@@ -9,28 +9,6 @@ import '../utils/url.dart';
 
 import './spinner.dart';
 
-class Songs extends StatefulWidget {
-  final String apiUrl;
-  final String artist;
-  final String album;
-  final void Function(int) onSelect;
-
-  Songs({
-    @required this.apiUrl,
-    @required this.artist, // can be an empty string
-    @required this.album, // can be an empty string
-    @required this.onSelect,
-  });
-
-  @override
-  _SongsWidgetState createState() => _SongsWidgetState(
-      apiUrl: this.apiUrl,
-      artist: this.artist,
-      album: this.album,
-      onSelect: this.onSelect,
-    );
-}
-
 Future<List<Song>> fetchSongs(String apiUrl, String artist) async {
   final response = await http.get(formattedUrl(apiUrl, '/songs', {
     'artist': artist,
@@ -48,29 +26,24 @@ Future<List<Song>> fetchSongs(String apiUrl, String artist) async {
   return songs;
 }
 
-class _SongsWidgetState extends State<Songs> {
-  final String apiUrl;
+class Songs extends StatelessWidget {
   final String artist;
   final String album;
-  Future<List<Song>> songs;
-
+  final Future<List<Song>> songs;
   final void Function(int) onSelect;
 
-  _SongsWidgetState({
-    @required this.apiUrl,
-    @required this.artist,
-    @required this.album,
+  Songs({
+    @required this.artist, // can be an empty string
+    @required this.album, // can be an empty string
+    @required this.songs,
     @required this.onSelect,
   });
 
-  @override
-  void initState() {
-    super.initState();
-    songs = fetchSongs(this.apiUrl, this.artist);
-  }
-
   @override
   Widget build(BuildContext context) {
+    if (artist == null || songs == null) {
+      return Container();
+    }
     return FutureBuilder<List<Song>>(
       future: songs,
       builder: (context, snapshot) {

+ 58 - 0
gmus-mobile/lib/components/statefullist.dart

@@ -0,0 +1,58 @@
+import 'package:flutter/widgets.dart';
+
+class DoubleHolder {
+  double value = 0.0;
+}
+
+class StatefulListView extends StatefulWidget {
+  final DoubleHolder offset = new DoubleHolder();
+  final int _itemCount;
+  final IndexedWidgetBuilder _indexedWidgetBuilder;
+
+  StatefulListView(
+    this._itemCount,
+    this._indexedWidgetBuilder,
+    { Key key }
+  ): super(key: key);
+
+  double getOffsetMethod() => offset.value;
+
+  void setOffsetMethod(double val) {
+    offset.value = val;
+  }
+
+  @override
+  _StatefulListViewState createState() => new _StatefulListViewState(_itemCount, _indexedWidgetBuilder);
+}
+
+class _StatefulListViewState extends State<StatefulListView> {
+  ScrollController scrollController;
+
+  final int _itemCount;
+  final IndexedWidgetBuilder _itemBuilder;
+
+  _StatefulListViewState(this._itemCount, this._itemBuilder);
+
+  @override
+  void initState() {
+    super.initState();
+    scrollController = new ScrollController(initialScrollOffset: widget.getOffsetMethod());
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return new NotificationListener(
+      child: new ListView.builder(
+        controller: scrollController,
+        itemCount: _itemCount,
+        itemBuilder: _itemBuilder,
+      ),
+      onNotification: (notification) {
+        if (notification is ScrollNotification) {
+          widget.setOffsetMethod(notification.metrics.pixels);
+        }
+        return;
+      },
+    );
+  }
+}

+ 1 - 1
gmus-mobile/lib/components/status.dart

@@ -14,7 +14,7 @@ class StatusBarWrapped extends StatelessWidget {
       return null;
     }
     return Obx(() {
-      var connected = controller.connected.value;
+      var connected = controller.socket.connected.value;
       if (!connected) {
         return Text("Disconnected");
       }

+ 21 - 14
gmus-mobile/lib/controller.dart

@@ -1,10 +1,11 @@
 import 'dart:convert';
+import 'dart:io';
 
 import 'package:get/get.dart';
-import 'package:web_socket_channel/io.dart';
 
 import './actions.dart' as actions;
 import './preferences.dart' as Preferences;
+import './socket.dart';
 
 class Player {
   double currentTime = 0;
@@ -38,23 +39,37 @@ class Client {
 
 class Controller extends GetxController {
   RxString apiUrl = ''.obs;
-  RxBool connected = false.obs;
 
   RxString name = ''.obs;
   RxString uniqueName = ''.obs;
 
-  IOWebSocketChannel channel;
-
   Rx<Player> player = new Player().obs;
   RxList<Client> clients = <Client>[].obs;
 
+  final Socket socket = Socket();
+
   Controller({
     this.apiUrl,
     this.name,
   });
 
+  Future<void> connect() async {
+    uniqueName.value = await socket.connect(
+      apiUrl.value,
+      name.value,
+      (message) => onRemoteMessage(this, message),
+    );
+  }
+
+  void disconnect() {
+    socket.disconnect();
+    uniqueName.value = '';
+  }
+
   setApiUrl(String apiUrl) {
+    this.disconnect();
     this.apiUrl = apiUrl.obs;
+    this.connect();
     Preferences.setApiUrl(apiUrl);
   }
 
@@ -67,21 +82,13 @@ class Controller extends GetxController {
     this.clients.assignAll(newClients);
   }
 
-  void _remoteDispatch(String action) {
-    if (this.channel == null) {
-      return;
-    }
-
-    this.channel.sink.add(action);
-  }
-
   bool _isMaster() => this.uniqueName.value == this.player.value.master;
 
   void playPause() {
     this.player.value.playing = !this.player.value.playing;
 
     if (!this._isMaster()) {
-      this._remoteDispatch(jsonEncode({
+      this.socket.dispatch(jsonEncode({
         'type': actions.STATE_SET,
         'payload': this.player.value.stringify(),
       }));
@@ -98,7 +105,7 @@ class Controller extends GetxController {
     this.player.value.seekTime = -1;
     this.player.value.playing = true;
 
-    this._remoteDispatch(jsonEncode({
+    this.socket.dispatch(jsonEncode({
       'type': actions.STATE_SET,
       'payload': this.player.value.stringify(),
     }));

+ 0 - 1
gmus-mobile/lib/preferences.dart

@@ -21,7 +21,6 @@ Future<Preferences> getPreferences() async {
 
 setApiUrl(String apiUrl) async {
   SharedPreferences preferences = await SharedPreferences.getInstance();
-  print('setting preferences apiUrl=$apiUrl');
   await preferences.setString('apiUrl', apiUrl);
 }
 

+ 73 - 48
gmus-mobile/lib/socket.dart

@@ -1,6 +1,8 @@
 import 'dart:async';
 import 'dart:convert';
+import 'dart:io';
 
+import 'package:get/get.dart';
 import 'package:nanoid/nanoid.dart';
 import 'package:web_socket_channel/io.dart';
 
@@ -17,17 +19,79 @@ String getUniqueName(String name) {
 
 const socketKeepaliveTimeoutMs = 30000;
 
-void keepalive(IOWebSocketChannel channel) {
-  var future = new Future.delayed(const Duration(milliseconds: socketKeepaliveTimeoutMs));
-  void ping(dynamic data) {
-    channel.sink.add(jsonEncode({'type': 'PING'}));
-    keepalive(channel);
+class Socket {
+  WebSocket conn;
+  IOWebSocketChannel channel;
+
+  RxBool connected = false.obs;
+  RxString error = ''.obs;
+
+  void _keepalive() {
+    var future = new Future.delayed(const Duration(milliseconds: socketKeepaliveTimeoutMs));
+    void ping(dynamic data) {
+      channel.sink.add(jsonEncode({'type': 'PING'}));
+      _keepalive();
+    }
+    var subscription = future.asStream().listen(ping);
+
+    channel.sink.done.whenComplete(() {
+      subscription.cancel();
+    });
+  }
+
+  Future<String> connect(
+    String apiUrl,
+    String name,
+    void Function(String) onRemoteMessage,
+  ) async {
+    if (apiUrl == null || name == null || apiUrl.length == 0 || name.length == 0) {
+      return null;
+    }
+
+    final String uniqueName = getUniqueName(name);
+
+    final String webSocketUrl = getWebSocketUrl(apiUrl);
+    final String pubsubUrl = "$webSocketUrl?client-name=$uniqueName";
+
+    connected.value = false;
+
+    try {
+      conn = await WebSocket
+        .connect(Uri.parse(pubsubUrl).toString())
+        .timeout(Duration(seconds: 10));
+
+      channel = IOWebSocketChannel(conn);
+
+      channel.stream.listen((message) => onRemoteMessage(message));
+
+      connected.value = true;
+      error.value = '';
+
+      _keepalive();
+    }
+    on SocketException {
+      error.value = "Error connecting to socket";
+    }
+    on TimeoutException {
+      error.value = "Timeout connecting to socket";
+    }
+    catch (err) {
+      error.value = "Unknown error connecting to socket";
+    }
+
+    return uniqueName;
+  }
+
+  void disconnect() {
+    conn?.close();
   }
-  var subscription = future.asStream().listen(ping);
 
-  channel.sink.done.whenComplete(() {
-    subscription.cancel();
-  });
+  void dispatch(String action) {
+    if (channel == null) {
+      return;
+    }
+    channel.sink.add(action);
+  }
 }
 
 void onRemoteMessage(Controller controller, String message) {
@@ -67,42 +131,3 @@ void onRemoteMessage(Controller controller, String message) {
       break;
   }
 }
-
-void connect(Controller controller) {
-  if (controller == null ||
-      controller.name.value.length == 0 ||
-      controller.apiUrl.value.length == 0) {
-    return;
-  }
-
-  final String uniqueName = getUniqueName(controller.name.value);
-  controller.uniqueName.value = uniqueName;
-
-  final String webSocketUrl = getWebSocketUrl(controller.apiUrl.value);
-  final String pubsubUrl = "$webSocketUrl?client-name=$uniqueName";
-
-  var channel = IOWebSocketChannel.connect(Uri.parse(pubsubUrl));
-  controller.channel = channel;
-
-  channel.stream.listen((message) {
-    onRemoteMessage(controller, message);
-  });
-
-  controller.connected.value = true;
-
-  keepalive(channel);
-}
-
-void disconnect(Controller controller) {
-  controller.connected.value = false;
-  if (controller.channel != null) {
-    controller.channel.sink.close();
-  }
-  controller.uniqueName.value = '';
-}
-
-void setApiUrl(Controller controller, String apiUrl) {
-  disconnect(controller);
-  controller.setApiUrl(apiUrl);
-  connect(controller);
-}

+ 1 - 1
gmus-mobile/test/components/status_test.dart

@@ -32,7 +32,7 @@ void main() {
   testWidgets('Status bar should render a connected message with name', (WidgetTester tester) async {
     Controller controller = Controller();
     controller.uniqueName = 'mob-DvaU1'.obs;
-    controller.connected.value = true;
+    controller.socket.connected.value = true;
 
     await tester.pumpWidget(TestStatusBar(controller: controller));
 

+ 6 - 7
gmus-mobile/test/controller_test.dart

@@ -60,7 +60,7 @@ void main() {
 
             controller.playPause();
 
-            verify(controller.channel.sink.add('{"type":"STATE_SET","payload":{"currentTime":0.0,"seekTime":-1.0,"master":"other-client-name-master","songId":null,"playing":true,"queue":[]}}')).called(1);
+            verify(controller.socket.channel.sink.add('{"type":"STATE_SET","payload":{"currentTime":0.0,"seekTime":-1.0,"master":"other-client-name-master","songId":null,"playing":true,"queue":[]}}')).called(1);
           });
         });
 
@@ -72,7 +72,7 @@ void main() {
 
             controller.playPause();
 
-            verify(controller.channel.sink.add('{"type":"STATE_SET","payload":{"currentTime":0.0,"seekTime":-1.0,"master":"other-client-name-master","songId":182,"playing":false,"queue":[]}}')).called(1);
+            verify(controller.socket.channel.sink.add('{"type":"STATE_SET","payload":{"currentTime":0.0,"seekTime":-1.0,"master":"other-client-name-master","songId":182,"playing":false,"queue":[]}}')).called(1);
           });
         });
       });
@@ -83,7 +83,7 @@ void main() {
           controller.player.value.playing = false;
           controller.playPause();
 
-          verifyNever(controller.channel.sink.add('{"type":"STATE_SET","payload":{"currentTime":0.0,"seekTime":-1.0,"master":"other-client-name-master","songId":null,"playing":true,"queue":[]}}'));
+          verifyNever(controller.socket.channel.sink.add('{"type":"STATE_SET","payload":{"currentTime":0.0,"seekTime":-1.0,"master":"other-client-name-master","songId":null,"playing":true,"queue":[]}}'));
         });
       });
     });
@@ -95,7 +95,7 @@ void main() {
 
           controller.playSong(871);
 
-          verify(controller.channel.sink.add('{"type":"STATE_SET","payload":{"currentTime":0.0,"seekTime":-1.0,"master":"other-client-name-master","songId":871,"playing":true,"queue":[]}}')).called(1);
+          verify(controller.socket.channel.sink.add('{"type":"STATE_SET","payload":{"currentTime":0.0,"seekTime":-1.0,"master":"other-client-name-master","songId":871,"playing":true,"queue":[]}}')).called(1);
         });
       });
 
@@ -124,7 +124,7 @@ Controller mockControllerAsMaster() {
   controllerAsMaster.uniqueName.value = 'my-client-name-master';
   controllerAsMaster.player.value.master = 'my-client-name-master';
 
-  controllerAsMaster.channel = MockChannel(sink: MockWebSocketSink());
+  controllerAsMaster.socket.channel = MockChannel(sink: MockWebSocketSink());
 
   return controllerAsMaster;
 }
@@ -134,8 +134,7 @@ Controller mockControllerAsSlave() {
   controllerAsSlave.uniqueName.value = 'my-client-name-slave';
   controllerAsSlave.player.value.master = 'other-client-name-master';
 
-  controllerAsSlave.channel = MockChannel(sink: MockWebSocketSink());
+  controllerAsSlave.socket.channel = MockChannel(sink: MockWebSocketSink());
 
   return controllerAsSlave;
 }
-