# Building a Very Good Location Sharing Chat App with Flutter

> How to build a chat app with location sharing functionality using Stream and Very Good Core

- Source: https://verygood.ventures/blog/very-good-location-sharing-chat-app/
- Published: 2021-09-29
- Author: Marcos Sevilla
- Tags: Architecture, Open Source, Tutorial

---

Ever imagined building a chat application like Facebook Messenger or WhatsApp? Typically, you would need to build a whole real-time infrastructure as a backend first, and then build your chat client for your users to start communicating. This could lead to a lot of time spent on development.

Building a Flutter chat application is surprisingly fast and easy thanks to our friends at [Stream](https://getstream.io/) and their [Stream Chat SDK](https://getstream.io/chat/sdk/flutter/), which provides us with all the functionality we need to hook up to a powerful chat API and start developing our app right away.

![Stream Chat SDK interface for Flutter chat integration](/assets/images/blog/very-good-location-sharing-chat-app/body-0.png)
*Stream Chat SDK*

We'll learn how to build a location-sharing chat app with Stream's real-time infrastructure and our opinionated set of tools (like [flutter_bloc](https://verygood.ventures/blog/why-we-use-flutter-bloc/)) and best practices. So, let's build a very good chat app!

## ChatRepository

First, we'll create a **ChatRepository** that will manage the chat domain. This repository will be created as a standalone package and will have the following dependencies:

```yaml
location: ^4.3.0
stream_chat_flutter: ^2.0.0
```

To use the Stream Chat SDK, we will provide our **ChatRepository** with a **StreamChatClient** that will act as our API in charge of interacting with the Stream backend. Additionally, we'll have a dependency on **Location** to have access to the device's location services, which will allow us to handle permissions and retrieve the current location data.

```dart
class ChatRepository {
  ChatRepository({
    StreamChatClient? chatClient,
    Location? location,
  })  : _chatClient = chatClient ?? StreamChatClient(''),
        _location = location ?? Location();

  final StreamChatClient _chatClient;
  final Location _location;
}
```

### StreamChatClient

We'll use our **StreamChatClient** to handle the connection and joining channels to chat.

The connect method must be the first to be called as it's the entry point for our user to be authenticated. It accepts extra information like the user's avatar, which can be provided as a **Uri**.

```dart
Future connect({
  required String userId,
  required String token,
  Uri? avatarUri,
}) {
  final extraData = {};
  if (avatarUri != null) {
    extraData['image'] = '$avatarUri';
  }
  return _chatClient.connectUser(
    User(id: userId, extraData: extraData),
    token,
  );
}
```

Now that our client has authenticated the user, we can access the **currentUser** via the client's state property. We will return the user id from this **getUserId** method for it to authenticate the user on the Stream Chat SDK widgets (you will see it in action later).

```dart
String getUserId() {
  final user = _chatClient.state.currentUser;
  if (user == null) {
    throw StateError(
      'could not retrieve user. did you forget to call connect()?',
    );
  }
  return user.id;
}
```

### Location

To share the user's location, we need to first retrieve it through our **Location** API. This is achieved by the **getCurrentLocation** method, which already handles asking for the location permission. The location is returned in the shape of a custom class called **CoordinatePair**.

```dart
Future getCurrentLocation() async {
  final serviceEnabled = await _location.serviceEnabled();
  if (!serviceEnabled) {
    final isEnabled = await _location.requestService();
    if (!isEnabled) throw CurrentLocationFailure();
  }

  final permissionStatus = await _location.hasPermission();
  if (permissionStatus == PermissionStatus.denied) {
    final status = await _location.requestPermission();
    if (status != PermissionStatus.granted) throw CurrentLocationFailure();
  }

  late final LocationData locationData;
  try {
    locationData = await _location.getLocation();
  } catch (_) {
    throw CurrentLocationFailure();
  }

  final latitude = locationData.latitude;
  final longitude = locationData.longitude;
  if (latitude == null || longitude == null) throw CurrentLocationFailure();

  return CoordinatePair(latitude: latitude, longitude: longitude);
}
```

## Chat UI

Now that we have our **ChatRepository**, it's time to create our second package: **chat\_ui**. This package will contain our widgets that will be abstracting the Stream Chat SDK widgets for our customization.

### ChannelAppBar

First, we will create a **ChannelAppBar** that will use **ChannelHeader** from Stream Chat under the hood. It looks like this:

```dart
class ChannelAppBar extends StatelessWidget with PreferredSizeWidget {
  const ChannelAppBar({
    Key? key,
    required stream_chat_flutter.Channel channel,
  })  : _channel = channel,
        super(key: key);

  final stream_chat_flutter.Channel _channel;

  @override
  Widget build(BuildContext context) {
    final channel = context
        .findAncestorStateOfType();
    if (channel != null) {
      return const stream_chat_flutter.ChannelHeader();
    }
    return stream_chat_flutter.StreamChannel(
      channel: _channel,
      child: const stream_chat_flutter.ChannelHeader(),
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
```

### ChannelListView

To build our channel list, we will abstract the Stream Chat's **ChannelListView** with its own **ChannelsBloc**. That way, we can provide the **ChannelsBloc** to any channel we create.

```dart
class ChannelListView extends StatelessWidget {
  const ChannelListView({
    Key? key,
    required this.userId,
    required this.channelBuilder,
  }) : super(key: key);

  final String userId;
  final Widget Function(
    BuildContext context,
    stream_chat_flutter.Channel channel,
  ) channelBuilder;

  @override
  Widget build(BuildContext context) {
    return stream_chat_flutter.ChannelsBloc(
      child: stream_chat_flutter.ChannelListView(
        filter: stream_chat_flutter.Filter.in_('members', [userId]),
        onChannelTap: (channel, child) {
          Navigator.of(context).push(
            MaterialPageRoute(
              builder: (_) => channelBuilder(context, channel),
            ),
          );
        },
      ),
    );
  }
}
```

We achieve this by specifying a function that we call **channelBuilder**, which is simply a callback with the context and the channel provided by Stream Chat's **ChannelListView**. This is also how we provide the channel that will be used on the next page to display the messages.

### MessageListView

Now we want to display the channel's messages. So we will do some wrapping of Stream Chat's **StreamChannel** and **MessageListView** widgets. Stay with me on this one, it's kind of tricky.

```dart
class MessageListView extends StatelessWidget {
  const MessageListView({
    Key? key,
    required stream_chat_flutter.Channel channel,
    OnGenerateAttachements? onGenerateAttachements,
  })  : _channel = channel,
        _onGenerateAttachements = onGenerateAttachements ?? const {},
        super(key: key);

  final stream_chat_flutter.Channel _channel;
  final OnGenerateAttachements _onGenerateAttachements;

  @override
  Widget build(BuildContext context) {
    final channel = context
        .findAncestorStateOfType();
    final customAttachmentBuilder = {};
    for (final entry in _onGenerateAttachements.entries) {
      customAttachmentBuilder[entry.key] = (context, details, attachments) {
        return entry.value(context, details);
      };
    }
    final messageListView = stream_chat_flutter.MessageListView(
      messageBuilder: (context, details, messages, widget) {
        return widget.copyWith(
          customAttachmentBuilders: customAttachmentBuilder,
        );
      },
    );
    if (channel != null) return messageListView;
    return stream_chat_flutter.StreamChannel(
      channel: _channel,
      child: messageListView,
    );
  }
}
```

First, we receive the corresponding channel from the **ChannelListView** we made earlier, which will help us to provide a channel for the **StreamChannel** widget to populate the messages list.

Since we want to add attachments such as location, we created a **typedef** called **OnGenerateAttachements** that is just a **Map** with a builder function. We populate the attachments of the messages based on what we get from the constructor.

At last, we create a **MessageListView** from the Stream Chat SDK and copy the attachments to its widget. We can build our **StreamChannel** widget with the channel and our recently created message list.

### MessageInput

To finish our UI package with the attachment and channel lookup logic, we need the last widget: a message input to type and send a new message to the channel.

```dart
class MessageInput extends StatefulWidget {
  const MessageInput({
    Key? key,
    required stream_chat_flutter.Channel channel,
    OnGenerateAttachementThumbnails? onGenerateAttachementThumbnails,
    List? actions,
    MessageInputController? controller,
  })  : _channel = channel,
        _onGenerateAttachementThumbnails =
            onGenerateAttachementThumbnails ?? const {},
        _actions = actions ?? const [],
        _controller = controller,
        super(key: key);

  final stream_chat_flutter.Channel _channel;
  final OnGenerateAttachementThumbnails _onGenerateAttachementThumbnails;
  final List _actions;
  final MessageInputController? _controller;

  @override
  _MessageInputState createState() => _MessageInputState();
}

class _MessageInputState extends State {
  final _messageInputKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    widget._controller?._key = _messageInputKey;
  }

  @override
  Widget build(BuildContext context) {
    final channel = context
        .findAncestorStateOfType();
    final messageInput = stream_chat_flutter.MessageInput(
      key: _messageInputKey,
      actions: widget._actions,
      attachmentThumbnailBuilders: widget._onGenerateAttachementThumbnails,
    );
    if (channel != null) return messageInput;
    return stream_chat_flutter.StreamChannel(
      channel: widget._channel,
      child: messageInput,
    );
  }
}
```

This widget is similar to **MessageListView**. It will wrap a **MessageInput** from the Stream Chat SDK. We will also add new functionality for actions, such as a share location button and the attachment thumbnail.

With that, we've finished our **chat\_ui** package. 🎉

## Building the App

### Bootstrapping

As we mentioned at the beginning, this app is created using our [Very Good CLI](https://pub.dev/packages/very_good_cli), which creates a project using the Very Good Core template.

In addition to what our CLI generates, we are going to create a new file called **bootstrap.dart**. This file will be in charge of initializing all of our dependencies and inject them into the app. This helps set up specific configurations before you run the app and allows us to initialize asynchronous dependencies. We set up the Stream Chat client by providing a **chatApiKey**. We then inject the Stream Chat SDK into the **ChatRepository**.

```dart
typedef AppBuilder = FutureOr Function(
  Widget Function(Widget) builder,
  ChatRepository chatRepository,
);

Future bootstrap({required AppBuilder builder}) async {
  Bloc.observer = AppBlocObserver();
  FlutterError.onError = (details) {
    log(details.exceptionAsString(), stackTrace: details.stack);
  };

  const chatApiKey = String.fromEnvironment('CHAT_API_KEY');
  if (chatApiKey.isEmpty) {
    throw StateError('missing environment variable ');
  }

  final chatClient = StreamChatClient(chatApiKey, logLevel: Level.OFF);
  final chatRepository = ChatRepository(chatClient: chatClient);

  const chatToken = String.fromEnvironment('CHAT_TOKEN');
  if (chatToken.isEmpty) {
    throw StateError('missing environment variable ');
  }

  const userId = '';
  final avatarUri = Uri.https(
    'getstream.imgix.net',
    'images/random_svg/FS.png',
  );

  await chatRepository.connect(
    userId: userId,
    token: chatToken,
    avatarUri: avatarUri,
  );

  await runZonedGuarded(
    () async => runApp(await builder(
      (child) => chat_ui.StreamChat(
        client: chatClient,
        child: child,
      ),
      chatRepository,
    )),
    (error, stackTrace) => log(error.toString(), stackTrace: stackTrace),
  );
}
```

### App Widget

```dart
class App extends StatelessWidget {
  const App({
    Key? key,
    required Widget Function(Widget) builder,
    required ChatRepository chatRepository,
  })  : _builder = builder,
        _chatRepository = chatRepository,
        super(key: key);

  final Widget Function(Widget) _builder;
  final ChatRepository _chatRepository;

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider.value(
      value: _chatRepository,
      child: MaterialApp(
        localizationsDelegates: const [
          AppLocalizations.delegate,
          GlobalMaterialLocalizations.delegate,
        ],
        supportedLocales: AppLocalizations.supportedLocales,
        home: const ChannelListPage(),
        builder: (_, child) => _builder(child!),
      ),
    );
  }
}
```

The **App** widget requires a widget builder and the **ChatRepository** that was provided during bootstrapping. We wrap the **MaterialApp** with a **RepositoryProvider** that injects the **ChatRepository** so we can access it from the subtree. Lastly, we'll add our home page and the **ChannelListPage**.

### Channel List

At Very Good Ventures, we separate our project by feature. Our first feature will be the channel list. Here, we will obtain the **userId** from the **ChatRepository** by using a [**Cubit**](https://bloclibrary.dev/#/coreconcepts?id=cubit) from the [**bloc** package](https://pub.dev/packages/bloc).

#### ChannelListState

```dart
class ChannelListState extends Equatable {
  const ChannelListState({required this.userId});

  final String userId;

  @override
  List get props => [userId];
}
```

#### ChannelListCubit

```dart
class ChannelListCubit extends Cubit {
  ChannelListCubit({
    required ChatRepository chatRepository,
  }) : super(ChannelListState(userId: chatRepository.getUserId()));
}
```

This **ChannelListCubit** will maintain the **userId** as part of its state, which we can provide to other widgets from the **chat\_ui** package.

#### ChannelListPage

**ChannelListPage** is composed of two widgets:

-   **ChannelListPage** handles the page's general structure and injects the **ChannelListCubit** into the widget tree.


```dart
class ChannelListPage extends StatelessWidget {
  const ChannelListPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final l10n = context.l10n;
    return Scaffold(
      appBar: AppBar(title: Text(l10n.channelListAppBarTitle)),
      body: BlocProvider(
        create: (context) => ChannelListCubit(
          chatRepository: context.read(),
        ),
        child: const ChannelListView(),
      ),
    );
  }
}
```

-   **ChannelListView** accesses the **userId** from the **ChannelListCubit** and provides it to the custom **ChannelListView** widget.

```dart
class ChannelListView extends StatelessWidget {
  const ChannelListView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final userId = context.select(
      (ChannelListCubit cubit) => cubit.state.userId,
    );
    return chat_ui.ChannelListView(
      userId: userId,
      channelBuilder: (_, channel) => MessageListPage(channel: channel),
    );
  }
}
```

As you can see, on the **ChannelListView** from **chat\_ui**, we use the **channelBuilder** property to provide the channel to the **MessageListPage**. That new page will be responsible for displaying the list of messages from the specified channel.

### Message List

#### MessageListState

The **MessageListCubit** will manage the selected channel, as well as the user's current location.

We will create the **CurrentLocation** class first:

```dart
enum CurrentLocationStatus { unavailable, available, pending }

class CurrentLocation extends Equatable {
  const CurrentLocation({
    this.latitude = 0.0,
    this.longtitude = 0.0,
    this.status = CurrentLocationStatus.pending,
  });

  final double latitude;
  final double longtitude;
  final CurrentLocationStatus status;

  @override
  List get props => [latitude, longtitude, status];

  CurrentLocation copyWith({
    double? latitude,
    double? longtitude,
    CurrentLocationStatus? status,
  }) {
    return CurrentLocation(
      latitude: latitude ?? this.latitude,
      longtitude: longtitude ?? this.longtitude,
      status: status ?? this.status,
    );
  }
}
```

Then, we can add it to the state:

```dart
class MessageListState extends Equatable {
  const MessageListState({
    required this.channel,
    this.location = const CurrentLocation(),
  });

  final Channel channel;
  final CurrentLocation location;

  @override
  List get props => [channel, location];

  MessageListState copyWith({Channel? channel, CurrentLocation? location}) {
    return MessageListState(
      channel: channel ?? this.channel,
      location: location ?? this.location,
    );
  }
}
```

#### MessageListCubit

Our new **Cubit** will have a dependency on the channel from the UI layer and the **ChatRepository**. We will expose a **locationRequested** method to get the current location of the device.

```dart
class MessageListCubit extends Cubit {
  MessageListCubit({
    required Channel channel,
    required ChatRepository chatRepository,
  })  : _chatRepository = chatRepository,
        super(MessageListState(channel: channel));

  final ChatRepository _chatRepository;

  void locationRequested() async {
    emit(
      state.copyWith(
        location: state.location.copyWith(
          status: CurrentLocationStatus.pending,
        ),
      ),
    );
    try {
      final location = await _chatRepository.getCurrentLocation();
      emit(
        state.copyWith(
          location: state.location.copyWith(
            status: CurrentLocationStatus.available,
            latitude: location.latitude,
            longtitude: location.longitude,
          ),
        ),
      );
    } catch (_) {
      emit(
        state.copyWith(
          location: state.location.copyWith(
            status: CurrentLocationStatus.unavailable,
          ),
        ),
      );
    }
  }
}
```

#### MessageListPage

The **ChannelListPage** is composed of a few widgets:

-   **MessageListPage** handles the page's general structure and injects the **MessageListCubit** into the widget tree.

```dart
class MessageListPage extends StatelessWidget {
  const MessageListPage({Key? key, required Channel channel})
      : _channel = channel,
        super(key: key);

  final Channel _channel;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ChannelAppBar(channel: _channel),
      body: BlocProvider(
        create: (context) => MessageListCubit(
          channel: _channel,
          chatRepository: context.read(),
        ),
        child: const MessageListView(),
      ),
    );
  }
}
```

-   **MessageListView** consumes the **MessageListCubit** by selecting the **channel** it contains and providing it to the custom **MessageListView** widget. This widget also uses a **BlocListener** and reacts to the **MessageListCubit** by either showing a **SnackBar** when location is not available or adding a location attachment when location is shared.

-   We use the **MessageInputController** from the **chat\_ui** package for the send functionality.

```dart
class MessageListView extends StatefulWidget {
  const MessageListView({Key? key}) : super(key: key);

  @override
  _MessageListViewState createState() => _MessageListViewState();
}

class _MessageListViewState extends State {
  late final chat_ui.MessageInputController _controller;

  @override
  void initState() {
    super.initState();
    _controller = chat_ui.MessageInputController();
  }

  @override
  Widget build(BuildContext context) {
    final channel = context.select(
      (MessageListCubit cubit) => cubit.state.channel,
    );
    return BlocListener(
      listenWhen: (prev, curr) => prev.location.status != curr.location.status,
      listener: (context, state) {
        if (state.location.status == CurrentLocationStatus.unavailable) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text(
                '''We can't access your location at this time. Did you allow location access?''',
              ),
            ),
          );
        }
        if (state.location.status == CurrentLocationStatus.available) {
          _controller.addAttachment(
            Attachment(
              type: 'location',
              uploadState: const UploadState.success(),
              extraData: {
                'latitude': state.location.latitude,
                'longitude': state.location.longtitude,
              },
            ),
          );
        }
      },
      child: Column(
        children: [
          Expanded(child: chat_ui.MessageListView(channel: channel)),
          chat_ui.MessageInput(
            controller: _controller,
            channel: channel,
            onGenerateAttachementThumbnails: {
              'location': (context, attachment) {
                return MapThumbnailImage(
                  latitude: attachment.extraData['latitude'] as double,
                  longitude: attachment.extraData['longitude'] as double,
                );
              },
            },
            actions: [
              IconButton(
                icon: const Icon(Icons.location_history),
                onPressed: () {
                  context.read().locationRequested();
                },
              ),
            ],
          ),
        ],
      ),
    );
  }
}
```

-   **MapThumbnailImage** extracts the map thumbnail image for the shared location message. This will be added to the channel as an attachment message each time the user shares their location.

```dart
class MapThumbnailImage extends StatelessWidget {
  const MapThumbnailImage({
    Key? key,
    required this.latitude,
    required this.longitude,
  }) : super(key: key);

  final double latitude;
  final double longitude;

  Uri get _thumbnailUri {
    return Uri(
      scheme: 'https',
      host: 'maps.googleapis.com',
      port: 443,
      path: '/maps/api/staticmap',
      queryParameters: {
        'center': '$latitude,$longitude',
        'zoom': '18',
        'size': '700x500',
        'maptype': 'roadmap',
        'key': '',
        'markers': 'color:red|$latitude,$longitude'
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Image.network(
      '$_thumbnailUri',
      height: 300.0,
      width: 600.0,
      fit: BoxFit.fill,
    );
  }
}
```

With these widgets, we can build a whole chat user interface to manage conversations (also called channels).

## Testing

[Testing](https://verygood.ventures/blog/guide-to-flutter-testing/) is an essential part of a successful and well-built app for us at VGV. Tests make it easy to know which parts of our code work properly. When it comes to changing existing functionality, tests can tell us if everything is still working as expected. We also have a very good playlist about [testing fundamentals](https://verygood.ventures/blog/testing-fundamentals-of-flutter-course/) by our teammate Jorge, so go check it out after reading this!

The chat application we just built has 100% code coverage, which means every part of it has at least one test written for it. We will go through the most notable ones so that you can take a look at how to write them.

There are three types of tests in our application: unit (isolated components), widget (UI components), and bloc (business logic components). We will go through an example of each one.

### Unit Testing ChatRepository

Unit testing aims to test the smallest pieces of code. Luckily Dart makes it a breeze to test these components with its [**test**](https://pub.dev/packages/test) package.

When we write a test, we first need to mock the dependencies of the component we are testing, if it has any. In this case, we're testing if the **ChatRepository** connects the user correctly. Our repository has a dependency on a **StreamChatClient** for this method, so we need to mock it.

```dart
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/stream_chat.dart';

class MockStreamChatClient extends Mock implements StreamChatClient {}
```

We create a Mock class using the [**mocktail**](https://pub.dev/packages/mocktail) package by extending the Mock class and implementing the class we want to mock (in this case, **StreamChatClient**). We can instantiate our chat client as a **MockStreamChatClient** and pass it to the **ChatRepository**.

```dart
group('ChatRepository', () {
  late StreamChatClient chatClient;
  late ChatRepository chatRepository;

  setUpAll(() {
    registerFallbackValue(FakeUser());
  });

  setUp(() {
    chatClient = MockStreamChatClient();
    chatRepository = ChatRepository(chatClient: chatClient);
  });
}
```

Now, we can stub the behavior we want for this dependency. For the connection functionality, we want to return a fake value that won't be stubbed, so we can create a **FakeOwnUser** class to return when the connect user method is called:

```dart
class FakeOwnUser extends Fake implements OwnUser {}
```

We use the **when** method to stub a fake user when the method is called with any kind of argument. Then, we ensure the method completes normally by using the **expect** method with the **completes** matcher, as it is a void function and won't return anything.

Lastly, we use the **verify** method to make sure our dependency was called only once with the correct arguments.

```dart
group('connect', () {
  const userId = 'test-user-id';
  const token = 'test-token';
  final avatarUri = Uri.https('test.com', 'profile/pic.png');

  test('connects with the provided userId, token, and avatarUri', () {
    when(
      () => chatClient.connectUser(any(), any()),
    ).thenAnswer((_) async => FakeOwnUser());

    expect(
      chatRepository.connect(
        userId: userId,
        token: token,
        avatarUri: avatarUri,
      ),
      completes,
    );

    verify(
      () => chatClient.connectUser(
        User(id: userId, extraData: {'image': '$avatarUri'}),
        token,
      ),
    ).called(1);
  });
});
```

### Widget Testing ChannelListView

Our presentation, or graphics components, should be tested too! Luckily, Flutter provides a complete framework to test this components easy with [**flutter_test**](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html).

Just as before, we must mock the dependencies of our widgets. For a **ChannelListView**, the dependencies are the **StreamChatClient** and the current **Channel**.

```dart
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'
    as stream_chat_flutter;

class MockStreamChatClient extends Mock
    implements stream_chat_flutter.StreamChatClient {}

class MockChannel extends Mock implements stream_chat_flutter.Channel {}

void main() {
  group('ChannelListView', () {
    late stream_chat_flutter.StreamChatClient client;

    setUp(() {
      client = MockStreamChatClient();
      when(
        () => client.on(any(), any(), any(), any()),
      ).thenAnswer((_) => const Stream.empty());
      when(
        () => client.wsConnectionStatus,
      ).thenReturn(stream_chat_flutter.ConnectionStatus.connected);
    });
  });
}
```

After we mocked and stubbed our dependencies, we can test our widgets.

```dart
testWidgets(
  'renders ChannelListView with correct configuration',
  (tester) async {
    const userId = 'test-user-id';
    await tester.pumpWidget(
      MaterialApp(
        home: stream_chat_flutter.StreamChat(
          client: client,
          child: Material(
            child: ChannelListView(
              userId: userId,
              channelBuilder: (context, channel) => const SizedBox(),
            ),
          ),
        ),
      ),
    );
    expect(find.byType(stream_chat_flutter.ChannelsBloc), findsOneWidget);
    final channelsListViewFinder =
        find.byType(stream_chat_flutter.ChannelListView);
    final channelsListView = tester
        .widget(channelsListViewFinder);
    final expectedFilter = stream_chat_flutter.Filter.in_(
      'members',
      const [userId],
    );
    expect(channelsListView.filter, equals(expectedFilter));
  },
);
```

A couple of things to note here:

-   We first use the **WidgetTester** provided by the **testWidgets** function, which allows us to build and interact with widgets by rendering them using the **pumpWidget** method.

-   After we render the widget we're testing, we use the **expect** method with a finder provided by the **flutter\_test** package. This is called **find.byType** which checks if one or more widgets of the specified type are found. We expect only one **ChannelsBloc**, so we use the **findsOneWidget** matcher.

-   We create a type finder for **ChannelListView** and use it to get the mentioned widget.

-   We create the expected filter for our **ChannelListView** and ensure it is rendered correctly.


### Bloc Testing MessageListCubit

We didn't forget about testing the business logic of the application. These components usually are harder to test, but don't worry: *there's a package for that*.

[**bloc_test**](https://pub.dev/packages/bloc_test) is part of the [bloc library](https://bloclibrary.dev/), and helps us a lot to write clear, simple bloc tests to get faster to 100% code coverage.

As always, we start with dependencies:

```dart
class MockChannel extends Mock implements Channel {}

class MockChatRepository extends Mock implements ChatRepository {}

void main() {
  group('MessageListCubit', () {
    late Channel channel;
    late ChatRepository chatRepository;

    setUp(() {
      channel = MockChannel();
      chatRepository = MockChatRepository();
    });
  });
}
```

After that, we can write our bloc test. We will test the "happy path" of the **locationRequested** method.

A bloc test helps us **build** our bloc with its dependencies, **act** by adding an event (bloc) or calling a method (cubit) that would emit some state, and **expect** a list of states that are going to be emitted.

*To learn more about* ***bloc\_test****, check the package documentation.*

```dart
const latitude = 42.0;
const longitude = 13.37;
const coordinatePair = CoordinatePair(
  latitude: latitude,
  longitude: longitude,
);

blocTest(
  'emits [pending, available] when location is available',
  build: () {
    when(
      () => chatRepository.getCurrentLocation(),
    ).thenAnswer((_) async => coordinatePair);
    return MessageListCubit(
      channel: channel,
      chatRepository: chatRepository,
    );
  },
  act: (cubit) => cubit.locationRequested(),
  expect: () => [
    MessageListState(channel: channel),
    MessageListState(
      channel: channel,
      location: const CurrentLocation(
        latitude: latitude,
        longtitude: longitude,
        status: CurrentLocationStatus.available,
      ),
    ),
  ],
);
```

Here, we stub our dependency and build the cubit with it. Then, call the method that will cause a new list of states to be emitted. And at last, specify the states we expect with their properties.

And there you go, a test for each layer of our app! 🧪

## Wrap Up

We finished building our chat application with location-sharing functionality! 🎉

This example was created using the [Stream Chat SDK](https://getstream.io/chat/sdk/flutter/) and [Very Good CLI](https://pub.dev/packages/very_good_cli) with our opinionated practices. You can take a look at the [full source code here](https://github.com/VGVentures/chat_location).

Hope you enjoyed going through this tutorial and we can't wait to see what you build next with these two packages together.

*Felix Angelov contributed to this article.*
