David Adegoke Well known for his skills and dynamic leadership, David has led development teams building and deploying great products. He is passionate about helping people learn mobile development with Flutter and the leadership strategies they need to succeed regardless of their background. As he says, "You just have to be consistent and intentional to make it."

Sharing content in Flutter apps using Share Plus

8 min read 2307

Flutter Share Content

If you’re an avid smartphone user, odds are you’ve seen something on a mobile application that you wanted to share with a friend, for example, while browsing through your favorite social media platform or exploring items for sale.

Giving your users the ability to share content from your app to other applications improves the overall user experience. In this article, you’ll learn how to implement content sharing in Flutter applications using the Share Plus plugin. To follow along with this article, you can check out the complete source code for the sample app. Let’s get started!

Table of contents

Sharing files, text, and images between apps

In this tutorial, we’ll cover how to share texts and images from our sample application to other mobile applications. We’ll fetch a list of fish species from the FishWatch API, display it in our app, then add the share functionality, enabling us to share information like the image, name, habitat, and quota of each fish specie with any other application. Let’s dive right in!

File sharing in operation: Sample application walkthrough

Project setup

Create a new codebase by running the command below:

flutter create flutter_share

The command above would create the base files that we’ll build upon for our sample application. Next, we’ll import the following dependencies in our pubspec.yaml file:

  • http: To make a GET request to the FishWatch API and retrieve the list of various fish species
  • stacked: An architectural solution that uses Provider under the hood, giving us access to classes that will spice up our development process
  • build_runner: Provides access to run commands for auto-generating files from annotations
  • stacked_generator: Generates files from stacked annotations
  • logger: Prints important information to the debug console
  • share_plus: The package we’ll use to implement the share functionality
  • permission_handler: Enables us to request permission to use the phone’s mobile storage
  • image_downloader: Enables us download the image file we want to share
dependencies:
      cupertinoicons: ^1.0.2
      flutter:
        sdk: flutter
      http: ^0.13.4
      imagedownloader: ^0.31.0
      logger: ^1.1.0
      permissionhandler: ^8.3.0
      shareplus: ^3.0.4
      stacked: ^2.2.8
    devdependencies:
      buildrunner: ^2.1.7
      flutterlints: ^1.0.0
      fluttertest:
        sdk: flutter
      stacked_generator: ^0.5.7

Setting up the models

Next, we’ll set up the models for converting the data we receive. From the FishWatch API documentation, we see that the API returns the name, description, image, and a host of other details. However, we only need the name, image, protein content, and quota.

Create a folder named models in the lib directory, which will hold all the models we’ll use. Create a file named fish_response_model.dart where we’ll create the FishResponseModel class and the ImageGallery model class:

class FishResponseModel {
  String? speciesName;
  String? protein;
  ImageGallery? speciesIllustrationPhoto;
  String? quote;
  FishResponseModel({
    required this.protein,
    required this.speciesIllustrationPhoto,
    required this.quote,
  });
  FishResponseModel.fromJson(Map<String, dynamic> json) {
    speciesName = json['Species Name'];
    protein = json['Protein'];
    quote = json['Quote'];
    speciesIllustrationPhoto =
        ImageGallery.fromJson(json['Species Illustration Photo']);
  }
}
class ImageGallery {
  String? src;
  String? alt;
  String? title;
  ImageGallery({this.src, this.alt, this.title});
  ImageGallery.fromJson(Map<String, dynamic> json) {
    src = json['src'];
    alt = json['alt'];
    title = json['title'];
  }
}

With this done, we can proceed to register the dependencies that we’ll use in the application.

Registering dependencies

We’ll register the services that will perform tasks in our application as dependencies. We’ll create the basic structure of each of the services in the section, then flesh them out in subsequent sections.

Create a folder called services in the lib directory, which will hold the services that we’ll use in the app. The ApiService handles all outbound connections from the application. Create a new file called api_service.dart. For now, we’ll only add the basic structure:

We made a custom demo for .
No really. Click here to check it out.

class ApiService {}

The FishSpeciesService service class will handle interactions with the FishWatch API. It will fetch the list of fish species and pass it into the app. Create a new file called fish_service.dart and put the basic class structure there:

class FishSpecieService {}

The ShareService service class will handle the share functionality, making it reusable in any part of the application. For the other services, create a new file called share_service.dart and place the basic class structure as follows:

class ShareService{}

Next, we’ll set up our routes and register the services. We’ll use the @StackedApp annotation, which comes from the Stacked package. The @StackedApp annotation grants us access to two parameters, routes and dependencies. We’ll use the dependencies block to register our services.

Create a folder called app in the lib directory, which will hold all the configuration details of our application. In this folder, create a new file named app.dart. Create an empty class and mark it with the @StackedApp annotation. Also pass in the StackedLogger to the logger parameter. Next, register the services as LazySingleton in the dependencies block:

import 'package:flutter_share/services/api_service.dart';
import 'package:flutter_share/services/fish_species_service.dart';
import 'package:flutter_share/services/share_service.dart';
import 'package:stacked/stacked_annotations.dart';
@StackedApp(
  dependencies: [
    LazySingleton(classType: ApiService),
    LazySingleton(classType: FishSpecieService),
    LazySingleton(classType: ShareService)
  ],
  logger: StackedLogger(),
)
class AppSetup {}

Now, we can go ahead and run the Flutter command to generate all of the necessary files for the setup configuration:

flutter pub run build_runner build --delete-conflicting-outputs

Setting up the services

Now that we’ve registered each of the dependencies, we can go ahead and fill them up starting with the ApiService. This service would handle all outbound HTTP requests from our application. It would have methods for each request type, GET, POST, PUT, PATCH, and DELETE. However, for this article, we’ll use a GET request to make a call to the FishWatch API endpoint.

Import the HTTP package as http. In the ApiService class, create a new function, which will accept a URL to which we’ll point our request. Make the call to the URL using the HTTP package, then check if the statusCode is 200. If it’s true, we’ll return the decodedResponse.

In addition, we wrap the entire call with a try...catch block in order to catch any exceptions that might be thrown. That’s basically everything in our ApiService file:

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
class ApiService {
  Future<dynamic> get(url) async {
    try {
      final response = await http.get(url);
      if (response.statusCode == 200) {
        return json.decode(response.body);
      }
    } on SocketException {
      rethrow;
    } on Exception catch (e) {
      throw Exception(e);
    }
  }
}

Next, we create a class to handle the constants relating to the API call, which will hold things like the scheme, host URL, URIs, etc. In the lib directory, create a new folder called utils and a new file called api_constants.dart:

class ApiConstants {
  static const scheme = 'https';
  static const baseUrl = 'fishwatch.gov';
  static const path = 'api/species';
  static get getFishSpecies => Uri(
        scheme: scheme,
        host: baseUrl,
        path: path,
      );
}

After this, the FishSpecieService, which makes the call to the remote API, gets the data and parses it using the models we created earlier:

import 'package:flutter_share/models/fish_response_model.dart';
import 'package:flutter_share/app/app.locator.dart';
import 'package:flutter_share/services/api_service.dart';
import 'package:flutter_share/utils/api_constants.dart';
class FishSpecieService {
  final _apiService = locator<ApiService>();
  Future<List<FishResponseModel?>?> getFishSpecie() async {
    List<FishResponseModel?> fishSpeciesList = [];
    try {
      final response = await _apiService.get(ApiConstants.getFishSpecies);
      if (response != null) {
        for (var i = 0; i < 15; i++) {
          final model = FishResponseModel.fromJson(response[i]);
          fishSpeciesList.add(model);
        }
        return fishSpeciesList;
      }
    } catch (e) {
      rethrow;
    }
  }
}

Finally, we create the ShareService, which would expose a method to share data from our application called onShare. It would take in the model of the particular fishSpecie from the list to be shared.

First we use the permission_handler package to request PermissionStatus of the storage permission. We need the local storage to save the image we want to share. We download the image, then share the actual image, not a link to the image.

In addition to this, in the AndroidManifest.xml, add the permission to write to the external storage, i.e., to download an image to the application local storage:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.flutter_share">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    ....
    </manifest>

Next, we check if the status is denied. If it is, we request for the permission.

Since we want to share an image file, and we are receiving the link to the image for each fish specie from the FishWatch API, we would download the image using the ImageDownloader package. We would call the downloadImage method on it and pass in the image URL from our model. We assign the response to a variable imageId, which will be the imageId for the downloaded image.

Next, we check if the imageId is null. If it is, we return and break out of the function. After confirming that the imageId is not null, we use the findPath method from ImageDownloader to get the path to the saved image on the local storage. This would be the path to the actual image file on your mobile device.

Finally, we call the shareFiles object from the share_plus package and pass the path to the imageId, as well as the quote as the subject, and a text showing the specie name and protein content:

import 'package:flutter_share/models/fish_response_model.dart';
import 'package:image_downloader/image_downloader.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';
class ShareService {
  Future<void> onShare(FishResponseModel fishSpecie) async {
    var status = await Permission.storage.status;
    if (status.isDenied) {
      await Permission.storage.request();
    }
    var imageId = await ImageDownloader.downloadImage(
        fishSpecie.speciesIllustrationPhoto!.src!);
    if (imageId == null) {
      return;
    }
    var path = await ImageDownloader.findPath(imageId);
    await Share.shareFiles(
      [path!],
      text: '''Specie Name: ${fishSpecie.speciesName}\n'''
          '''Protein Content: ${fishSpecie.protein}\n''',
      subject: fishSpecie.quote,
    );
  }
}

With that, we’re done setting up our ShareService. We can use this service to implement the share functionality anywhere in our application.

Next, head over to the main.dart file. In the main block, we would call the WidgetsFlutterBinding.ensureInitialized(), then call the setUpLocator function, which will set up the locator making all registered dependencies available to us throughout our application:

import 'package:flutter/material.dart';
import 'package:flutter_share/app/app.locator.dart';
import 'package:flutter_share/ui/views/home/home_view.dart';
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  setupLocator();
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Material App',
      home: HomeView(),
    );
  }
}

At this stage, we’ve successfully laid the foundation for the share functionality. Now, we can go ahead and build the user interface and finally use the service.

Implementing the share functionality

Create a new folder called ui in the lib directory, which will contain the user interfaces in the application. We’ll only have one, the home_view. Create a new folder named home_view where we’ll store two files, home_view.dart and home_viewmodel.dart, which would hold the UI code and the logic for the UI, respectively.

In the home_viewmodel.dart file, create a class named HomeViewModel. It will have two functions, one to getFishSpecie from the API using the FishSpecieService and the other to share the selected fishSpecie.

We’ll access the respective services using the locator, then use the services to fetch the respective data:

import 'package:flutter/material.dart';
import 'package:flutter_share/app/app.locator.dart';
import 'package:flutter_share/models/fish_response_model.dart';
import 'package:flutter_share/services/fish_species_service.dart';
import 'package:flutter_share/services/share_service.dart';
import 'package:stacked/stacked.dart';

class HomeViewModel extends BaseViewModel {
  final _fishSpecieService = locator<FishSpecieService>();
  final _shareService = locator<ShareService>();
  List<FishResponseModel?>? fishSpeciesList = [];

  Future<void> getFishSpecies() async {
    try {
      fishSpeciesList = await runBusyFuture(
        _fishSpecieService.getFishSpecie(),
        throwException: true,
      );
      notifyListeners();
    } on Exception catch (e) {
      debugPrint(e.toString());
    }
  }

  Future<void> shareFishSeries(FishResponseModel fishSeries) async {
    try {
      await _shareService.onShare(fishSeries);
    } on Exception catch (e) {
      debugPrint(e.toString());
    }
  }

}

Building the UI

Finally, in the home_view.dart file, create a stateless widget, which would be the base for this view. The HomeView would display the list of fish species gotten and stored into the fishesSpeciesList in the HomeViewModel.

The most important piece of the HomeView is the share button, which we’ll use to trigger the share functionality. It would simply call the shareFishSeries function in the HomeViewModel, passing to it the fishSpecieModel of the specie that was selected:

   Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                const Icon(Icons.share, size: 18),
                const SizedBox(width: 8),
                GestureDetector(
                  onTap: () {
                    homeViewModel!.shareFishSeries(fishSpecieModel!);
                  },
                  child: const Text(
                    'Share',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                )
              ],
            )

Check our the complete code for the HomeView in this gist.

With this, our sample app is good to go! Let’s build and run the app to see as it all comes together:

https://youtu.be/dohGK3KzYAU

Conclusion

In this tutorial, we’ve successfully pulled data from an API, then shared details of that data with other applications on our phone. You can now share images and texts from your application with other applications. You can also feel free to build more on this application and customize it to your desired look and feel.

I hope you enjoyed this tutorial! Happy coding. If you have any questions, feel free to leave a comment or reach out to me on Twitter @Blazebrain or LinkedIn.

David Adegoke Well known for his skills and dynamic leadership, David has led development teams building and deploying great products. He is passionate about helping people learn mobile development with Flutter and the leadership strategies they need to succeed regardless of their background. As he says, "You just have to be consistent and intentional to make it."

Leave a Reply