【Dart/Flutter】パッケージ化のすゝめ その１

# はじめに

株式会社ORPHE（オルフェ）の廣瀬です。
今回はDart/Flutterでパッケージ化をすることのメリットとその方法についてお伝えしたいと思います。

# Dartパッケージとは

Dart / Flutterでアプリを作成する際に特に意識せずに使っているかと思いますが、`pubspec.yaml`の`dependencies`セクションで読み込むライブラリのことを`パッケージ`と呼びます。
Flutterもパッケージ群をインポートしており`StatelessWidget`や`StatefulWidget`といったFlutterの根幹の機能もすべてFlutter用のパッケージから利用されています。

[pub.dev](https://pub.dev)などからインポートする[intl](https://pub.dev/packages/intl)や[Provider](https://pub.dev/packages/provider)といったライブラリもすべてこのパッケージの仕組として作成されており自由にインポート可能です。

## パッケージの種類

パッケージには２つの種類があります。

### Dart Package

Dartのみで実装された一般的なパッケージ。AndroidやiOSなどのプラットフォームに依存せずに様々な機能を提供します。

### Plugin Package

Dartのみではなく、AndroidやiOS向けのネイティブのAPIにアクセスする機能も含めたパッケージ。利用するネイティブの機能によって利用可能なプラットフォームが異なります。

# パッケージ化のメリット・デメリット

パッケージ化には下記のメリットデメリットがありますが、基本的にはメリットのほうが大きいので積極的にパッケージ化していったほうがオススメです。

## メリット

- 機能別にコードを整理できるためコードの保守性が高まる
- すでに作成したコードや機能の使い回しが可能
- パッケージ化しやすいように設計を考えることができるようになるため分かりやすい設計構造になりやすい

## デメリット

- デバッグなどで不具合が起こった場合、パッケージを横断して修正しなければいけない可能性がある
- うまく機能を分けられていない場合パッケージ間の整合性が取れず破綻する可能性がある

パッケージ化を行うときの設計方法や考え方に関してはまた別記事で紹介したいと思います。

# パッケージ化の方法

具体的にパッケージ化を行うための方法を紹介していきます。

今回はDartコードを機能別に分けて利用する目的として`Dart Package`タイプのパッケージとして作成したいと思います。

## パッケージプロジェクトを作ろう

下記のコマンドを叩いてみましょう

```bash
flutter create --template=package パッケージ名
```

今回はパッケージ名を`test_package`とします。

```bash
flutter create --template=package test_package
```

Flutterプロジェクトを作成するときのようにパッケージに必要なファイルが作成されます。

基本的にはFlutterのアプリを作成するときと同じ様に`pubspec.yaml`に他のパッケージの依存関係を記載し`libフォルダ以下`にソースコードを作成して開発を進めていきます。

## コードを書いてみよう

lib/test_package.dartに下記のサンプルコードが作成されていると思います。

```dart
library test_package;

/// A Calculator.
class Calculator {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}
```

こちらを書き換えてみましょう。

Flutterプロジェクトを作成するときに下記のカウントアップアプリのサンプルコードが作成されると思いますが、こちらの`MyHomePage`ウィジェットをそのままパッケージの機能として使えるようにしてみます。

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```

lib/test_package.dartを下記の様に書き換えてみましょう。

```dart
library test_package;

import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  const CounterPage({Key? key, required this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```

`Calculator`のクラスを削除してしまったことで`test/test_package_test.dart`にエラーが出てしまっているかと思うのでこちらを削除します。

これで`CounterPage`ウィジェットを利用可能なパッケージを作成できました。

## 別プロジェクトでインポートしよう

それでは作成したパッケージをインポートするためのプロジェクトを作成してみましょう。

```bash
flutter create test_project
```

下記のようなフォルダ構成になるようにします。

```
root
├─test_package
│  │
│  ├─lib
│  │      test_package.dart
│  │
│  │  pubspec.lock
│  │  pubspec.yaml
│  └─ README.md
│
└─test_project
    │
    ├─android
    ├─ios
    ├─web
    ├─lib
    │      main.dart
    │
    │  pubspec.lock
    │  pubspec.yaml
    └─ README.md
```

`test_project/pubspec.yaml`の`dependences`以下を下記のように書き換えます。

```yaml
dependencies:
  flutter:
    sdk: flutter

  test_package:
    path: ../test_package
```

下記のコマンドを実行します。

```bash
cd test_project
flutter pub get
```

 これで`test_package`が利用可能になりました。

`test_project/lib/main.dart`を書き換えてみましょう。

```dart
import 'dart:developer';

import 'package:flutter/material.dart';

// パッケージをインポートします。
import 'package:test_package/test_package.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      // CounterPageを利用します。
      home: const CounterPage(title: 'Flutter Demo Home Page'),
    );
  }
}
```

これでビルドしてみると問題なく`CounterPage`を利用しながらアプリの起動ができるはずです！

### インポートの種類

pubspec.yamlでのインポート方法には今回のローカルに置かれているパッケージのインポートの他にもいくつか種類があります。

- [pub.dev](https://pub.dev)からのインポート
    Providerやintlなどの[pub.dev](https://pub.dev)に公開されているパッケージをインポートする方法です。
    
    パッケージ名をそのまま入力します。
    
    ```yaml
    dependencies:
      flutter:
        sdk: flutter
    
      provider: （バージョン※オプション）
    ```
    
- gitからのインポート
    
    GithubやBitbucketといったgitのホスティングサービスからのインポートが可能です。
    
    ※基本的にはパブリックレポジトリをインポートすることができます。SSH経由でアクセスしたり認証を通すことによってプライベートレポジトリからもインポート可能ですがここでは割愛します。
    
    ```yaml
    dependencies:
      flutter:
        sdk: flutter
    
      test_package:
    		url: git://github.com/orphe/test_package.git
    ```
    

# まとめ

いかがでしたでしょうか？
パッケージ化を行うことで実装した機能を分割し使い回しができるようになります。パッケージの作成やインポートも通常のプロジェクトを作成するのと同じ様に簡単に行うことができます。

次回は、有効なパッケージ化のやり方を記事にできたらと思います。

株式会社ORPHEではFlutterエンジニアを募集しています。
興味ある方は是非応募していただけると幸いです。

[株式会社ORPHEの会社情報 - Wantedly](https://www.wantedly.com/companies/orphe)