kakueki61's dev history

備忘録的に記録を残しています

Flutter: 遷移先画面を終了時に値を受け取る

遷移先の画面を終了する時に、元の画面へ何かしらのデータを渡したいというケースがあります。例えば、遷移先の画面で何か選択をした結果を元の画面へ反映したい時などです。
AndroidであればstartActivityForResultでActivityを起動し、元画面のonActivityResultでデータを受け取るという感じで実装することができます。

FlutterではNavigator.pushFutureを返します。遷移先のNavigator.popはこのFutureをcompleteさせますので、遷移元の画面はFutureへコールバックを登録しておくことでデータを受け取ることができます。

Flutter公式ドキュメント
Return data from a screen
を元にして実装方法を紹介します。

1. 遷移元画面作成

ボタン押下時に画面遷移させるようにします。

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Returnning Data Demo'),
      ),
      body: new Builder(
        builder: (BuildContext context) {
          return new Center(
            child: new RaisedButton(
              onPressed: () {
                _navigateAndDisplaySelection(context);
              },
              child: new Text('Pick an option, any option!'),
            )
          );
        }
      )
    );
  }

  _navigateAndDisplaySelection(BuildContext context) {
    Navigator.push(context, new MaterialPageRoute(builder: (context) => new SelectionScreen()))
  }
}

2. 遷移先画面を作成

"Up!"と"Down!"と書かれた2つのボタンを用意します。

class SelectionScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: new Text('Pick an option'),),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Padding(
              padding: const EdgeInsets.all(8.0),
              child: new RaisedButton(
                onPressed: () {
                  Navigator.pop(context, 'Up!');
                },
                child: new Text('Up!'),),
            ),
            new Padding(
              padding: const EdgeInsets.all(8.0),
              child: new RaisedButton(
                onPressed: () {
                  Navigator.pop(context, 'Down!');
                },
                child: new Text('Down!')
              )
            )
          ],
        )
      ),
    );
  }
}

各ボタン押下時にNavigator.popでこの画面を終了する共に、遷移前の画面へ"Up!"と"Down!"の文字列を渡すようにしています。

3. 遷移元画面で値を受け取る

HomeScreen_navigateAndDisplaySelectionを以下のように修正します。

_navigateAndDisplaySelection(BuildContext context) {
  Navigator.push(
    context,
    new MaterialPageRoute(builder: (context) => new SelectionScreen())
  ).then((result) {
    Scaffold.of(context).showSnackBar(new SnackBar(content: new Text('$result')));
  });
}

前述のようにNavigator.pushFutureを返すのでthenでコールバックを登録することができます。
resultにはNavigator.popで渡されたデータが渡ってきてコールバックが実行されます。
ここではSnackBarでその文字列を表示しています。

async / awaitで書く

thenでコールバック登録するような記述は煩雑になりがちなので、以下のように書くことが多いです。

_navigateAndDisplaySelection(BuildContext context) async {
  final result = await Navigator.push(
    context,
    new MaterialPageRoute(builder: (context) => new SelectionScreen())
  );

  Scaffold.of(context).showSnackBar(new SnackBar(content: new Text('$result')));
}

Navigator.pushFutureが生成されますが、awaitによってFutureがcompleteするまで処理が中断状態になります。前述のようにNavigator.popでデータが渡されると処理が再開し、resultにデータが代入され、そのデータでSnackBarを表示させるという流れになっています。

この辺りのFutureの扱いに関しては
Asynchronous Programming: Futures
が参考になるかと思います。

最後に、AndroidとiOSでビルドすると以下のようになります。

Android iOS
f:id:kakueki61:20180418013852g:plain f:id:kakueki61:20180418013740g:plain

Flutter: 画面遷移で次画面へ値を渡す

画面をタップして次画面へ遷移する時に値を渡す方法を紹介します。
AndroidであればIntentでActivity間の値の受け渡しをすることができます。
Flutterで画面遷移する時には、次画面にあたるWidgetのオブジェクトを生成します。このWidgetのコンストラクタで値を渡すことができます。

以下に簡単な例を示します。

1. 遷移元画面を用意

「A」「B」「C」と書かれた3つのボタンを用意します。

import 'package:flutter/material.dart';

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('First screen'),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Padding(
              padding: const EdgeInsets.all(8.0),
              child: new RaisedButton(
                child: new Text('A'),
                textColor: Colors.white,
                color: Colors.lightBlue,
                onPressed: () {
                  _navigateToNext(context);
                }
              )
            ),
            new Padding(
              padding: const EdgeInsets.all(8.0),
              child: new RaisedButton(
                child: new Text('B'),
                textColor: Colors.white,
                color: Colors.lightBlue,
                onPressed: () {
                  _navigateToNext(context);
                }
              )
            ),
            new Padding(
              padding: const EdgeInsets.all(8.0),
              child: new RaisedButton(
                child: new Text('C'),
                textColor: Colors.white,
                color: Colors.lightBlue,
                onPressed: () {
                  _navigateToNext(context);
                }
              )
            )
          ]
        )
      )
    );
  }

  void _navigateToNext(BuildContext context) {
    // 次画面へ遷移して値を渡す
  }
}

2. 遷移先の画面を用意

中央に受け取った値を表示するTextと元の画面へ戻るためのボタンを配置します。

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

class DetailScreen extends StatelessWidget {

  final String text;

  DetailScreen({Key key, @required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Detail screen'),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Padding(
              padding: const EdgeInsets.all(8.0),
              child: new Text(
                  '$text',
                  style: new TextStyle(
                    fontSize: 50.0
                  ),
              )
            ),
            new RaisedButton(
              child: new Text('Back'),
              textColor: Colors.white,
              color: Colors.lightBlue,
              onPressed: () => Navigator.pop(context)
            )
          ]
        )
      )
    );
  }
}

@requiredアノテーションで文字列textを必須としています。

3. 遷移先へ文字列を渡す

FirstScreenを以下のように修正します。

onPressed: () {
  _navigateToNext(context, 'A');
}
void _navigateToNext(BuildContext context, String text) {
  Navigator.push(context, new MaterialPageRoute(builder: (context) =>
    new DetailScreen(text: text)
  ));
}

ボタンによって_navigateToNextへ渡す文字列を変えれば、DetailScreenで表示される文字列も変化します。

これでAndroidとiOSでビルドすると以下のような感じになります。

Android iOS
f:id:kakueki61:20180414015215g:plain f:id:kakueki61:20180414015234g:plain

Flutterで画面遷移

Flutterの公式ドキュメント
Navigate to a new screen and back
を元にFlutterで画面遷移をどう書くか紹介します。

Navigatorクラス

アプリの「画面」にあたるものは、AndroidではActivityであり、iOSではViewControllerです。
Flutterで画面にあたるものはrouteと呼ばれていて、NavigatorRouteオブジェクトのスタックを管理します。 Navigatorがスタックに対してRouteをプッシュしたり、ポップしたりすることで画面遷移を実現するというのが基本的な考え方です。

2つの画面間を遷移する

2つの画面を用意

最初の画面としてHomeScreenというクラスを用意します。
画面の中央に”Next”と表示されたボタンが置いてあるだけです。

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Navigator Demo',
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: new HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Navigator Demo Home Screen")
      ),
      body: new Center(
        child: new RaisedButton(
          child: new Text('Next'),
          textColor: Colors.white,
          color: Colors.lightBlue,
          onPressed: () {
            // ボタン押下時の処理
          }
        )
      )
    );
  }
}

次に遷移先のNextScreenです。こちらは”Back"と書かれたボタンが置かれているだけす。

import 'package:flutter/material.dart';

class NextScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Next Screen'),
      ),
      body: new Center(
        child: new RaisedButton(
          child: new Text('Back'),
          textColor: Colors.white,
          color: Colors.lightBlue,
          onPressed: () {}
        )
      )
    );
  }
}

Navigator.pushで次の画面へ遷移

HomeScreenのNextボタンを押した時にNavigator.pushメソッドを使ってNextScreenへ遷移させます。 onPressedの処理にNavigator.pushを追加します。

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

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      ...
          onPressed: () {
            Navigator.push(
              context,
              new MaterialPageRoute(builder: (context) => new NextScreen())
            );
          }
        )
      )
    );
  }
}

Navigator.pushRouteオブジェクトをプッシュしてスタックに追加します。 RouteオブジェクトとしてMaterialPageRouteを利用しました。
Routeは遷移の見た目に関する情報も持っていますが、MaterialPageRouteはプラットフォームに応じた遷移アニメーションを行ってくれます。

Navigator.popで元の画面へ遷移

”Back"ボタンを押したら元の画面へ戻るようにします。Navigator.popメソッドで今のRouteオブジェクトをポップしてスタックから取り除きます。

class NextScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      ...
          onPressed: () {
            Navigator.pop(context);
          }
        )
      )
    );
  }
}

AndroidとiOSでビルドすると以下のような感じになります。

Android iOS
f:id:kakueki61:20180413013428g:plain f:id:kakueki61:20180413013436g:plain

Flutterでアプリを作ってみる(導入編)

仕事では主にKotlinを使ってAndroidアプリ開発をしています。
プライベートでもコードを書いたりするのですが、ふと思いついたアイデアを形にしたいと思い、アプリを作ったりすることがあります。
とりあえず動くものをiOSかAndroidで書くことはできるのですが、後々のことを考えるとiOS/Androidの両方を自分で書くのはツライよなあと思ったりもします。
そういうこともあってワンソースでiOS/Androidの両方に対応できるクロスプラットフォームなフレームワークに興味があり、最近ではReact Nativeを試していました。
ところがどうにもしっくりこず、いまいち進捗が芳しくありませんでした。。
そういったところで最近よく話を聞くようになったFlutterを試してみることにしました。

Flutterって?

FlutterDart言語によってiOS/Android両プラットフォームで動くアプリを書けるGoogleのフレームワークです。

様々な特徴がありますが、React Nativeを試していた時に比べていくつか大きく心惹かれるポイントがありました。

開発環境

普段Android開発を行っていることもあり、Android Studioという非常に高機能なIDEでの開発にどっぷりとつかってしまっています。
Android StudioやIntelliJとそのプラグインで開発ができるというのはとても魅力に感じました。

それほどがっつりJavaScriptを書かない私からすると、React Nativeでは開発環境を整えるだけで一つのハードルがあり、プライベートのプログラミングではそういったことが開発モチベーションを削ぐ大きな要因になったりします。
それを取り除けたことは大きかったです。

Dart言語

私は静的型付け言語が好きです。 基本的にはいつどこでどんなデータが扱われているのかが明確で、動的型付け言語よりもコードを読むのも書くのも楽だと感じています。

見た目はJavaのようです。 最近はKotlinを書くことが多いのでJavaに戻ってきてしまったような感覚でそこは若干あれな気分ですが、全然よいです。

最近の盛り上がり

Flutterでの開発事例や最近ベータ版をリリースしたというニュースもあり、注目が集まっています。 アルファとしてリリースされてから多くの機能が追加され、まだ未成熟とはいえ今後も盛り上がっていく機運が感じられたのでトライしてみようと思いました。

導入

macOS + Android Studioでの導入を前提に書きます。

インストール

Gitリポジトリからクローンしてきます。

$ git clone -b beta https://github.com/flutter/flutter.git

次に足りてないものがないか確認します

$ flutter doctor
...
[] Flutter (Channel beta, v0.1.5, on Mac OS X 10.13.3 17D47, locale en-JP)
[] Android toolchain - develop for Android devices (Android SDK 27.0.3)
[!] iOS toolchain - develop for iOS devices (Xcode 9.2)
    ✗ libimobiledevice and ideviceinstaller are not installed. To install, run:
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    ✗ ios-deploy not installed. To install:
        brew install ios-deploy
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
        For more info, see https://flutter.io/platform-plugins
      To install:
        brew install cocoapods
        pod setup
[✓] Android Studio (version 3.0)
[!] VS Code (version 1.20.1)
[!] Connected devices
    ! No devices available

! Doctor found issues in 3 categories.

iOS toolchainがないという結果がでました。これらはiOS実機にアプリをインストールする際に必要になりますが、今は無視します。 その他にVS Codeについてもなんか出てますが、使わないので無視します。デバイスが接続されてないとも出ていますが、これも今は関係ないので無視します。

IDE環境

Android Studioのインストール

インストールしましょう。

エミュレーターを準備

やってない人は準備しましょう。 https://developer.android.com/studio/run/managing-avds.html

プラグイン導入

Preferences>Pluginsでプラグインの設定画面を開きBrowse repositories…ボタンを押します。
Flutterというプラグインがありますので、検索します。
installボタンを押すと、Dartプラグインもインストールするかと聞かれるのでYesを選択します。
インストールが終わったら、ガイドにしたがってAndroid Studioを再起動しましょう。

プロジェクト作成

以下の画面でStart a new Flutter projectを選択、あるいはメニューからFile>New Flutter Projectでプロジェクトを作成します。
f:id:kakueki61:20180317011345p:plain:w300

ビルドしてみる

f:id:kakueki61:20180317012606p:plain:w400
Android Studioの上部にこんなメニューがあると思います。 一番左がインストール先のデバイスです。先程用意したエミュレーターを選択します。 一番右のボタンが実行なのでこれを押すとビルドが始まり、無事にインストールされるとエミュレーターに以下のような画面が現れます。

f:id:kakueki61:20180317013033p:plain:w300

ホットリロードを試してみる

Flutterの大きな特徴の一つにホットリロードというものがあります。
これはコードを編集するとその変更が即アプリに反映されるというものです。再ビルド&再インストールをすることなくこれが実現されます。

試しにコードを編集してその過程を見てみます。 f:id:kakueki61:20180317014356g:plain

このように起動中のアプリに対して即座に変更が反映されます。

まとめ

以上、Flutterに関して簡単な説明と導入方法を書きました。 今後は実際の開発方法を紹介していければと思います。

React Nativeでアプリを作ってみる(導入編)

React NativeはJavaScriptでiOS/Androidアプリを開発するためのフレームワークです。 Facebook社のJavaScriptライブラリであるReactを基礎としています。
基本的にはJavaScriptで書くことができますので、ワンソースでiOSアプリとAndroidアプリを同時に開発できるのが大きな利点です。
JavaScriptでマルチプラットフォームというと、WebViewアプリであったりハイブリッドアプリを思い浮かべたりしますが、ReactNativeではWebView上でアプリを動かすというわけではありません。
JavaやSwiftで開発したものにかなり近いものを開発することができます(もちろん限界もありますが)。

The React Native CLI

create-react-native-appを使ってお手軽に開発をスタートさせることができますが、ここではより広範な開発をできるReact Native CLIを使った方法を紹介します。
なお、紹介するのはMac環境での手順となります。

インストール

1. HomeBrewをインストール(もししてなかったら)

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

2. nodeをインストール

brew install node

3. watchmanをインストール

開発時のコード変更を監視するのに使います。コード修正を素早くデバッグ中のアプリに反映させたりするのに使います。

brew install watchman

4. react-native-cliをインストール

npm install -g react-native-cli

どこでも使えるようにグローバルに置いときます。 インストールはsudoが必要かもしれません。

プロジェクト作成

react-native init HogeProject

このコマンドで以下のようなファイル郡が生成されます。

.
├── App.js
├── __tests__
│   └── App.js
├── android
│   ├── app
│   ├── build.gradle
│   ├── gradle
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── keystores
│   └── settings.gradle
├── app.json
├── index.js
├── ios
│   ├── HogeProject
│   ├── HogeProject-tvOS
│   ├── HogeProject-tvOSTests
│   ├── HogeProject.xcodeproj
│   └── HogeProjectTests
├── node_modules
│   ├── ...
├── package-lock.json
└── package.json

/android/iosディレクトリ内には各プラットフォーム用の雛形が作成されます。 index.jsがReactのコードになります。 node_modulesにはJavaScriptの依存パッケージが入っています。

起動してみる

iOS

事前準備

  1. Xcodeのインストール
  2. Command Line Toolsのインストール

起動

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

react-native run-ios

ビルドが成功すると、このようにシミュレータが起動します。 f:id:kakueki61:20180122204836p:plain:w300

Android

事前準備

  1. Java Development Kit(JDK)のインストール
    JDK 8をDLしてインストールします。JDK 9だとビルドできないので注意してください。
  2. Android開発環境
    Android Studioをインストールし、Android SDK, Android SDK Platform, Performance (Intel ® HAXM), Android Virtual Deviceもインストールします。
    詳細は要件はこちらを参照してください。
  3. 環境変数の設定
    React NativeのコマンドラインツールがAndroid SDKを利用するので以下の内容を$HOME/.bash_profileに書いておきます。
  export ANDROID_HOME=$HOME/Library/Android/sdk
  export PATH=$PATH:$ANDROID_HOME/tools
  export PATH=$PATH:$ANDROID_HOME/platform-tools

source $HOME/.bash_profileで設定を読み込んでください。

起動

リアル端末をMacに接続、あるいはエミュレーターを立ち上げた状態で(エミュレーターの詳細はこちらを見て下さい)、以下のコマンドを実行します。

react-native run-android

ビルドに成功すると端末は以下のような画面になります。

f:id:kakueki61:20180122205525p:plain:w300

まとめ

今回はreact-nativeコマンドでのプロジェクト作成とビルド方法を紹介しました。

個人でアプリを作ろうと思ったのですが、その時にiOSとAndroidの両方を一人で書くのはツライなあと感じて興味をもったのがReact Nativeでした。
私も勉強中ですが、今後もその中で得られたことを紹介していければと思います。

【Android】CursorLoaderのSQLに関して

たとえば以下のようなCursorLoaderを継承したクラスを作った時に、CursorLoaderのコンストラクタの引数にどんな風にSQLを渡せばいいのかよくわかんなかったので調べました。

public class GalleryLoader extends CursorLoader{

    private static final String[] PROJECTION = {
            MediaStore.Images.Media.BUCKET_ID
    };
    private static final String SELECTION =
            MediaStore.Files.FileColumns.MEDIA_TYPE + "=" + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
            + " OR " +
            MediaStore.Files.FileColumns.MEDIA_TYPE + "=" + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO;

    public GalleryLoader(Context context) {
        super(context,
                MediaStore.Files.getContentUri("external"),
                PROJECTION,
                SELECTION,
                null,
                MediaStore.Files.FileColumns.DATE_ADDED + " DESC");
    }

    @Override
    public Cursor loadInBackground() {
        return super.loadInBackground();
    }
}

これはローカルの端末のリソースから画像と動画を取り出すためのものですが、その中でも"アルバム"を取得しようとしている部分です。 やりたいこととしては、GROUP BY句を入れるということです。 なぜならこのままだとリソースの数分だけレコードが返ってしまう。まずはアルバムだけ取得したいという感じです。

問題点としては、CursorLoaderのコンストラクタを見てもらえればわかります。

    public CursorLoader(Context context, Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        super(context);
        mObserver = new ForceLoadContentObserver();
        mUri = uri;
        mProjection = projection;
        mSelection = selection;
        mSelectionArgs = selectionArgs;
        mSortOrder = sortOrder;
    }

ContentResolver.query())

Where句に渡す引数はあるんですが、GROUP BYをどうやって挿入すればいいのかわからない。

ということで実際にどのようなSQLが発行されているのか見てみました。 私が書いたようなGalleryLoaderでは以下のようなSQLが発行されていました。

SELECT bucket_id FROM files WHERE ( alive=1 ) AND (media_type=1 OR media_type=3) ORDER BY date_added DESC

ここに無理やりGROUP BYを挿入しようとすると、WHEREにうまいこと渡してやればよさそうです。 というわけで、

private static final String GROUP_BY = ") GROUP BY " + MediaStore.Images.Media.BUCKET_ID + ", (2";

といった感じにして

    public GalleryLoader(Context context) {
        super(context,
                MediaStore.Files.getContentUri("external"),
                PROJECTION,
                SELECTION + GROUP_BY,
                null,
                MediaStore.Files.FileColumns.DATE_ADDED + " DESC");
    }

と書き直しました。 これで期待通りの挙動になっています。

Androidでmultipart/form-dataをPOST送信しながら、POSTの進捗をActivityに通知する。

[Draft] 前回、multipart/form-dataでファイルなどをPOSTする方法を書きましたが、

Androidでサーバーに画像データをPOSTする - kakueki61's dev history

アップロード中に進捗表示をしてあげた方がユーザーフレンドリーだろうということで、 進捗表示を実装しましたので書いておきたい思います。

やったこととしては、

  1. multipart/form-dataの形式に則ってデータを構築
  2. HttpUrlConnectionからOutputStreamを取得
  3. データをOutputStreamに流す
  4. どれだけ流しおえたかをActivityに通知する。

前回のおさらい

org.apache.http.entity.mime.MultipartEntityBuilderでmultipart/form-data用のデータを作成。
自作のHurlStack派生クラスを作成し、performRequest()をoverride.
performRequest()内で、HttpClient#execute(HttpPost request)によってMultipartFormEntityをPOST。

今回、データアップロード中の進捗を表示をするために問題だったのは、
HttpClientがネットワークへのデータ書き出しの部分を隠蔽してしまっているので
そこをこちらでハンドリングするのが困難だという点でした。
ので、HttpClientを使うのはやめてHttpURLConnectionを使うようにします。
実際、Volleyでデフォルトで使われているHurlStackではHttpURLConnectionを使っています。

HttpURLConnectionを使うと大変なこと

MultipartEntityBuilderが使えません。
つまり、multipart/form-data用のデータをHTTPの仕様に沿って自前で構築する必要があります。 実際にどのような形式のデータを送る必要があるかはこちらを参考にして下さい。

MultipartJsonRequest.java

public class MultipartJsonRequest extends JsonRequest<JSONObject> {
    private static final String TAG = MultipartJsonRequest.class.getSimpleName();
    private static final String BOUNDARY = "___________________" + Long.toString(System.currentTimeMillis());

    private Activity mActivity;
    private Map<String, String> mStringParams;
    private Map<String, InputStream> mBinaryParams;
    private INetworkProgressListener mProgressListener;
    private Handler mHandler;

    public MultipartJsonRequest(Activity activity, String url, Map<String, String> stringParams, Map<String, InputStream> bynaryParams,
                                INetworkProgressListener progressListener,
                                Response.Listener<JSONObject> listener,
                                Response.ErrorListener errorListener) {
        
        super(activity, url, null, false, listener, errorListener);

        mStringParams = stringParams;
        mBinaryParams = bynaryParams;
        mProgressListener = progressListener;

        // Activityに通知するためのHandler
        mHandler = new Handler(Looper.getMainLooper(), null);
    }

    public Map<String, String> getStringParams() {
        return mStringParams;
    }

    public Map<String, InputStream> getBinaryParams() {
        return mBinaryParams;
    }

    @Override
    public String getBodyContentType() {
        return "multipart/form-data; boundary=" + BOUNDARY + "; charset=UTF-8";
    }

    public String getBoundaryString() {
        return BOUNDARY;
    }

    public void deliverProgress(final int numCompleted) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mProgressListener.onNetworkProgress(numCompleted);
            }
        });
    }

    @Override
    protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
        // 省略
    }
}

MultipartHurlStack.java

    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {
        if (!(request instanceof MultipartJsonRequest)) {
            return super.performRequest(request, additionalHeaders);
        }

        mMultipartRequest = (MultipartJsonRequest) request;
        mBoundary = mMultipartRequest.getBoundaryString();

        URL url = new URL(mMultipartRequest.getUrl());
        HttpURLConnection connection = openConnection(url, request);

        addHeaders(connection, additionalHeaders);
        addHeaders(connection, request.getHeaders());

        connection.setChunkedStreamingMode(0);

        DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());

        try {
            writeStringParams(outputStream, mMultipartRequest.getStringParams());
            writeBinaryParams(outputStream, mMultipartRequest.getBinaryParams());

            finishWriting(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
            throw new IOException("IOException: during writing requests");
        }
        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
        int responseCode = connection.getResponseCode();
        if (responseCode == -1) {
            // -1 is returned by getResponseCode() if the response code could not be retrieved.
            // Signal to the caller that something was wrong with the connection.
            throw new IOException("Could not retrieve response code from HttpUrlConnection.");
        }
        StatusLine responseStatus = new BasicStatusLine(protocolVersion,
                connection.getResponseCode(), connection.getResponseMessage());
        BasicHttpResponse response = new BasicHttpResponse(responseStatus);
        response.setEntity(entityFromConnection(connection));
        for (Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
            if (header.getKey() != null) {
                Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
                response.addHeader(h);
            }
        }
        return response;
    }

    private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        int timeoutMs = request.getTimeoutMs();     //TODO いる?
    LogHelper.i(TAG, "timeoutMs: " + timeoutMs);
        connection.setConnectTimeout(timeoutMs);
        connection.setReadTimeout(timeoutMs);

        connection.setUseCaches(false);
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type", mMultipartRequest.getBodyContentType());

        return connection;
    }

    private void writeStringParams(DataOutputStream outputStream, Map<String, String> stringParams) throws IOException {
        StringBuilder builder = new StringBuilder();
        for (Map.Entry<String, String> entry : stringParams.entrySet()) {
            builder.append(TWO_HYPHENS + mBoundary + LINE_END);
            builder.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINE_END);
            builder.append(LINE_END);
            builder.append(entry.getValue());
            builder.append(LINE_END);
        }
        LogHelper.d(TAG, "stringParams: \n" + builder.toString());

        outputStream.write(builder.toString().getBytes(Constants.DEFAULT_CHARSET));
        outputStream.flush();
    }

    private void writeBinaryParams(DataOutputStream outputStream, Map<String, InputStream> binaryParams) throws IOException {
        int numCompleted = 0;
        for (Map.Entry<String, InputStream> entry : binaryParams.entrySet()) {
            StringBuilder builder = new StringBuilder();
            builder.append(TWO_HYPHENS + mBoundary + LINE_END);
            builder.append("Content-Disposition: form-data; " +
                    "name=\"uploadFiles[]\"; " +
                    "filename=\"" + entry.getKey() + "\"" + LINE_END);
            builder.append("Content-Type: multipart/form-data" + LINE_END);     //TODO content-type確認
            builder.append(LINE_END);
    LogHelper.d(TAG, "binaryParams: \n" + builder.toString());

            outputStream.writeBytes(builder.toString());
            outputStream.flush();

            InputStream inputStream = entry.getValue();
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int readBytes = -1;
            while((readBytes = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, readBytes);
            }
            outputStream.flush();
            inputStream.close();

            outputStream.writeBytes(LINE_END);
            outputStream.flush();
    LogHelper.i(TAG, "finished!!!!");

            numCompleted++;
            mMultipartRequest.deliverProgress(numCompleted);
        }
    }

    private void finishWriting(DataOutputStream outputStream) throws IOException {
        StringBuilder builder = new StringBuilder();
        builder.append(TWO_HYPHENS + mBoundary + TWO_HYPHENS + LINE_END);
        outputStream.writeBytes(builder.toString());
        outputStream.flush();
        outputStream.close();
    }

    private void addHeaders(HttpURLConnection conn, Map<String, String> headers) {
        for (String key : headers.keySet()) {
            conn.setRequestProperty(key, headers.get(key));
        }
    }

続きは近いうちに書きます。


参考サイト

Upload files by sending multipart request programmatically

HTTPマルチパートデータ送信 « androidnote