Wowgineer Notes

WowTechエンジニアの徒然開発日記

Wowgineer Notes

〜新たな"Wow"を生み出す〜

新卒がFlutterで自作アプリ開発をしてみた!〜Flutter を選んだ理由と実際に開発をしてみて〜

新卒がFlutter で自作アプリ開発をしてみた!

こんにちは、新卒3期生として7月からアプリ開発チームに配属されました、スギヤマです!
今回は開発に慣れるためにFlutterで開発してみて学んだことを紹介します。
この記事はFlutterに興味を持ち始めた人や新卒がアプリ開発する際になぜFlutterを選択したのかを知りたい人向けの記事です。

1.なぜFlutterを使用したのか

今回、開発するにあたってFlutterを選定したのかというと、将来性が高いフレームワークと言われているからです。実際にGoogleトレンドで調べてみると、検索率が高いことが以下のグラフでも一目瞭然です。

ReactNativeとFlutterとXamarinのトレンド動向

【1つ目:効率的にアプリを開発できること】
Flutterにはアプリケーションを停止したり、再起動したりせずに修正を反映できる「Hot Reload」機能があります。コードをアプリにすぐに反映できるため、開発時間を大幅に削減できるのが特徴です。
またFlutterで開発するとAndroidとiOSを同じコードで開発できるため、本来二倍かかるはずだった人件費と時間を削減できます。

【2つ目:学習する量が少ない】
Flutterで使用している言語はDartです。
Dartは、JavaScriptに替わる言語だと期待され開発された背景があるため、文法が非常に似ています。これにより、Flutterによる開発をする場合、JavaやJavaScriptを学んだ人にとっては、学習ハードルが低く、比較的理解しやすいのが特徴です。

2.Flutter とReactNativeとXamarinを比較してみた

Flutter とReactNativeとXamarinの比較

比較対象 メリット デメリット
Flutter ・ウィジェットが豊富
・コミュニティが急速に成長している
・公式ドキュメントが非常に充実している
・テストが少なくて済む
・アプリが速い。ホットリロードでリアルタイムで変更点を逐一把握できる
・Webやデスクトップ(Windows、macOSやLinux)に対応したアプリを開発できる
・ネイティブ開発と比較すると、スマートフォンの最新機能が使えないなどいくらか劣る
・AndroidとiOS両方に対応したアプリケーションを開発できることを想定して開発されているので、アプリの容量が非常に大きい
・ネイティブ開発と比較して、ツールやライブラリのセットが限定される
ReactNative ・ホットリロード機能がある
・JavaScriptの知識と経験があれば簡単に学習・開発できる
・コードを再利用できる
・コミュニティ活動が活発である
・世界の有名なアプリケーションに使われている
・実績があり、Flutterよりも歴史が長くビジネス面で信頼されている
・Flutterと同様にネイティブ開発に劣る場面がある
・モバイル開発にしか特化していないので、Flutterと比較して選択肢が限られる
・パッケージとライブラリの中には、長期間メンテナンスされていないものが少なくない
・Flutterと同様に、クロスプラットフォームに対応しているのでアプリケーションの容量がネイティブより大きい
Xamarin ・ネイティブコードにコンパイルされるため、高いパフォーマンスである
・C#を使用し、.NETライブラリと統合してクロスプラットフォームアプリケーションを開発可能
・ネイティブUIコントロールへのアクセスが容易で、各プラットフォームの特徴を活かせる
・共通のコードベースを使用することで、複数プラットフォームの開発を効率的に行える
・React NativeやFlutterに比べてコミュニティが小さく、アップデートが遅れることがある
・ネイティブコードへのコンパイルが必要で、プラットフォームごとに設定が必要
・ウェブプラットフォームへの対応は限定的であり、ウェブアプリケーションの開発には向いていない


トレンドや発展性から考えるとFlutter を用いることが良いと考えられます。一方で、Flutterの動作環境には違いがあるため、注意が必要です。

3.FlutterでAndroid、iOS、Mac/PC、Webの開発する際の特徴

【Android・iOSの特徴】
FlutterはAndroid・iOS両方の開発が一度でできますが、例えば、カメラや通知などのOS固有の機能は、各OSごとに個別に開発する必要があります。さらに、OSアップデートがある場合、OSの新機能などを利用するためにはFlutter のアップデートやサポートされるライブラリーが開発されるのを待つ必要もあるため、場合によっては独自に開発することが求められます。

【Mac/PCの特徴】
ネイティブのウェブアプリケーションは、HTML、CSS、JavaScriptといったウェブテクノロジーを使用して作成されます。一方で、Flutterのウェブデスクトップアプリケーションは、Flutterの独自のUIフレームワークを使用して構築されます。これにより、ネイティブのウェブテクノロジーとの適合性に制約が生じることがあります。

【Webの特徴】
ウェブアプリケーションは、異なるブラウザ間での互換性に注意が必要です。一部のブラウザでは正しく表示されない、動作しない、または予期しない結果をもたらすことがあります。一部のライブラリーが対応していない場合があるため注意が必要です。

4.実際に自作アプリを作ってみた

(1)何を作ったのか
今回作ったアプリは「スーパーお得情報シェアアプリ」です。
いろんな店舗に行かずとも、今日の野菜や商品の値段をアプリ上で比較して最安値の店舗を知れるアプリを作りました。

自作アプリ「スーパーお得情報シェアアプリ」

(2)取り組んだ背景、目的
なぜ、このアプリを作ったのかというと野菜や果物といった食べ物はどこの店が一番安いのかわからず店を行ったり来たりした経験があり、ランキングで安い食べ物を知ることができるアプリが欲しいと考えたからです。

(3)難しかったところ、課題点とどう対処したか
難しかったところはデータの整合性を保つことが難しかったことです。

【実現したいこと】
・登録画面で商品を登録するとリストに商品名だけが表示され、その商品名を押した遷移先ではお店の情報と値段の情報がランキングで表示されるようにしたい。
・新たに同じ商品名を登録したときはリストに同じ表品名が並ぶことなくランキング画面で値段の安い順に表示されるようにしたい。

作成中の問題点は同じ商品名を登録して更新すると同じ名前の商品がリストに並んでしまうので、登録されたデータの整合性を保つ方法について考えることが、私にとっては課題でした。

対処として、同じproductNameがあった場合は既存のインスタンスに追加し、新しいproductNameの場合は新たにインスタンスを作るといった、条件を分けてあげることで解決しました。また、1回目の条件if{}の続きに2回目の条件をelse{}で処理を書かないことがキーポイントでした。

前提として、ShopProductはお店の情報と該当の商品名を保持します。Productは商品の情報を保持します。
itemListはProductの配列、newShopProductは渡されてきた新しい情報が入ります。

例えば、else{}で続きの処理を書いたBAD例

for (Product p in itemList) {
 if (p.productName == newShopProduct.productName) {
  p.shopProductList.add(newShopProduct); 
    }
   else {
  itemList.add(
   Product(
    productName: newShopProduct.productName,
    shopProductList: [newShopProduct]
   )
  );
 }
}
setState(() {});


この実装をすると、同じ名前が既にあったとしても配列全ての項目を判定する前に次の処理に進んでしまうため、同じ名前がないと判断されてしまいます。

それを阻止するために以下の実装をします。

bool isExist = false; 
for (Product p in itemList) {
 if (p.productName == newShopProduct.productName) {
  isExist = true;
  p.shopProductList.add(newShopProduct); 
  break;
 }
}
if (!isExist) {
 itemList.add(
  Product(
   productName: newShopProduct.productName,
   shopProductList: [newShopProduct]
  )
 );
}
setState(() {});

このように、実装することで配列全ての項目をチェックした後に次の処理に進むため、同じ名前があったとしても配列に追加されずに理想の形にすることができました。

(4)まとめ
初めてのアプリ開発でほぼ、上司と同期に助けていただきながら製作しましたが、新しい発見と学びだらけで目が回りそうな開発期間でした。今回学んだことを忘れずにいち早く貢献できるように次回からも頑張っていきたいです。

参考資料

・「ionic, React Native, Flutter, Cordova - 調べる」
https://trends.google.co.jp/trends/explore?q=ionic,React%20Native,Flutter,Cordova&geo=JP,JP,JP,JP&date=today%2012-m,today%2012-m,today%2012-m,today%2012-m#GEO_MAP (2023.8.10最終アクセス)
・「React Native vs. Flutter vs. Ionic vs. Xamarin vs. Nativescript (A Detailed Comparison)」
https://inapp-inc.medium.com/react-native-vs-flutter-vs-ionic-vs-xamarin-vs-nativescript-a-detailed-comparison-bfa99d72c4bb (2022.10.31)
・「Flutterの将来性は?メリット・デメリットや特徴も紹介!」
https://tech.hipro-job.jp/column/5874 (2023.06.09)
・「Flutter / フラッターとは?メリット、デメリット、特徴、将来性を解説」
https://genee.jp/contents/tech0003/(2022.07.25)
・「【徹底比較】Flutter VS React Native」
https://zenn.dev/nameless_sn/articles/flutter_vs_reactnative(202.11.21)
・「クロスプラットフォーム開発手法の比較(Xamarin、Flutter、React Native)」
https://qiita.com/tonionagauzzi/items/9a57bf876b7e956065a4 (2019.12.24)

Kingsoftは随時新メンバーを募集中ですので、一緒に働ける方をお待ちしております!

f:id:wowtech-dev:20190313163146j:plain

ChatGPTの活用法と新卒社会人が開発した献立提案アプリ

こんにちは!新卒3期生として7月からアプリ開発チームに配属されました遠藤です。

この記事では、食材管理とChatGPTの連携による献立提案アプリを紹介します。
このアプリの概要やシステム構成、技術的課題の克服方法について、さらに初めてのFlutter開発やChatGPT連携を通じての成長体験もまとめています。ChatGPT連携に興味があるけど、具体的な使い方が分からないという人に向けて書いていますので、ぜひご覧ください!

何を作ったか

このアプリは、購入した食べ物のリストを元にChatGPTが献立を提案してくれるというアプリです。ユーザーは購入した食材の名前や個数、消費期限を入力し、登録したリストから献立を検索できます。ChatGPTとは、ユーザーの質問に対してAIが生成した文章を返してくれるという生成AIの一種です。ChatGPTとのAPI連携により、ユーザーが登録した食べ物を元に最適な献立を提案することができるため、無駄なく食べ物を消費することができます。

使い方

取り組んだ背景と目的

今回開発したアプリは私が本配属後初めて取り組んだものです。今後の開発でFlutterを使用していき慣れる必要があり、取り組みました。
また、ChatGPT関連の機能を使ってみたいなと思っていたため、このアプリ制作を通じてChatGPTとの連携をやってみようと思いました。
 このアプリを作った目的は、今年4月から一人暮らしを始めて、お得な食べ物を購入しても使い切れずに結局は捨ててしまうということが何回かあり、何とかしたいと思っていたことと、食費を節約でき、食品ロスを防ぐことにつながるということは多くの人にとっても有用なのではないかと思い、作ることにしました。

FlutterとChatGPTの連携

以下が今回私が開発したアプリのシークエンス図です。

献立提案アプリのシークエンス図

製品レベルのプロジェクトでは、アプリ内にAPIキーなどの認証情報を埋め込むことはせず、セキュアなサーバーを利用して通信を仲介するのが一般的ですが、今回は研修用のため、アプリ内に直接ChatGPTのapiキーを持たせて作りました。

class MyHttpRequest {
  static const _apiKey = "";

  Future<List<Recipe>> getRecipes(List<Food> foods, int requestCount) async { //Future<List<Recipe>>
    String openApiKey = _apiKey;
    String foodList = "";
    for (var food in foods) {
      foodList += " ${food.name} \n";
    }
    String systemPrompt = """
    # 命令
    あなたは主婦の視点を持った料理研究家として、優先度が高い順に並んだ食べ物から作成されたレシピを3品教えてくれるようにプログラムされています。
    しかし必ず指示された食べ物全てを使わないといけないという訳ではありません。
    また、調味料などは自由に使って構いません。その際は献立で使う材料として追加して下さい。

    ## Input Format:
    $foodList


    """;
https://platform.openai.com/docs/guides/gpt/how-should-i-set-the-temperature-parameter
    var url = Uri.parse('https://api.openai.com/v1/chat/completions');
    var body = jsonEncode({     
      'model': 'gpt-3.5-turbo',// or 'model': 'gpt-4',
      'messages': [
        {'role': 'user', 'content': foodList},
        {'role': 'system', 'content': systemPrompt}
      ],
      'functions':[
        {
          "name": "get_recipes",
          "description": "複数の食べ物のリストを基にレシピの提案を3品してください。食べ物リストが少なすぎる場合、素直に'わからない'と答えてください。",
          "parameters": {
            "type": "object",
            "properties": {
              "recipe1": {
                "type": "object",
                "properties":{
                  "menu": {
                      "type": "string",
                      "description": "提案する3つのレシピのうち1品目のレシピの名前。例:'豚肉の生姜焼き'",
                  },
                  "ingredients": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties":{
                          "name":{
                            "type": "string",
                            "description": "提案する3つのレシピのうち1品目のレシピで使う材料の名前。例1:豚ロース, 例2:玉ねぎ, 例3:薄力粉, 例4:すりおろし生姜, 例5:醤油, 例6:砂糖, 例7:料理酒, 例8:油"
                          },
                          "quantity":{
                            "type": "string",
                            "description": "提案する3つのレシピのうち1品目のレシピで使う材料の量。例1:100g, 例2: 1/4個, 例3:適量, 例4:小さじ1, 例5:大さじ1, 例6:小さじ1, 例7:大さじ1, 例8:小さじ1/2"
                          },
                      },
                    "required": ["name","quantity"],  
                    },
                    "description": "提案する3つのレシピのうち1品目のレシピで使う材料の配列。",  
                  },
                  "process": {
                    "type": "string",
                    "description": "提案する3つのレシピのうち1品目のレシピの作り方。例:'1. 豚ロースに薄力粉をまぶす。\n2. ボウルにたれの材料(すりおろし生姜, 醤油, 砂糖, 料理酒)を入れて混ぜ合わせる。\n3. 中火で熱したフライパンにごま油をひき、1を入れて焼く。豚ロースに火が通り、三日月型に切った玉ねぎを入れる。\n4.豚肉の両面にこんがりと焼き色が付いたら2を入れる。\n5.中火で炒め合わせ、全体に味がなじんだら完成。'",
                  },
              
                },
              },    
              "recipe2":{
                //以下省略
              },
              "recipe3":{
                //以下省略
              }
            },
            "required": ["recipe1","recipe2","recipe3"],    
          }
          
        }
      ],
      "function_call": "auto",
      "temperature": 0.1,
    });

    var response = await http.post(
      url, 
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $openApiKey',
      },
      body: body,
    );
  }
}

難しかったところ、課題点とどう対処したか

まず、初めて使うChatGPTとの連携において、いくつか壁がありました。
一つ目は、ChatGPTからのレスポンスのときに、アプリで表示するためのフォーマットである「jsonフォーマット」で返すよう指定したにも関わらず高確率でテキストで返され、正しく情報を取得できないという問題が起きました。しかし、調べていくと、「Function Calling」という方法を見つけ、これを使うことで正しくレスポンスを取得することができました。これについて詳しく説明します。

Function Callingについて

Function Callingは、他のサービスの情報を元にChatGPT本来の自然言語で返すために使います。例えば、ChatGPTはリアルタイムの情報がないため今日の天気を聞いても答えることができません。しかし、今日の天気の情報を取得できるプラグインを用いて取得した情報を基にChatGPTに質問することで、自然言語でリアルタイムの天気の情報を送ってくれ、ユーザ側ではChatGPTからリアルタイムの天気の情報が送られることになります。
上記のコードは、Function Callingを用いてpostリクエストしたコードです。postリクエストを送るときにbodyという中身にchatGPTのバージョン情報や受け取りたい情報のフォーマットの指定、その他chatGPTの設定情報などを与えます。functionsというパラメータで「get_recipes」と名付けた関数を作成しています。function_callというパラメータをautoとすることで、実行される関数が自動で選択されます。今回はfunctionsで作成している関数は一つしかないので、autoにしても必然的に一つの関数に決まるのですが、複数関数を作成すると中身の情報を見て自動的に関数を選択し、欲しいフォーマットに沿った情報を返してくれるそうです。

今回のアプリでは、本来のFunction Callingの目的の使い方はせず、欲しいフォーマットで返してもらうという目的で使いました。 Function Callingの使い方に関しては、以下の公式ドキュメントを参考に作成しました。

Function Callingの使い方 platform.openai.com

次に、レスポンスのフォーマットが正しくなったのはいいものの、今度は一部の情報が空で返ってくるという課題に直面しました。この課題については、調べて試してもなかなか改善できなかったのと、そもそも概念的な理解が難しかったため、日々の業務でChatGPTの開発を行っている陳さんからアドバイスをいただきました。具体的な対処法としては、以下の3つの点を改善しました。

方法1:ChatGPTのバージョンをアップデートする
方法2:温度(temperature)プロパティの調整
方法3:functionsで指定するプロパティ「description」の説明を詳細に記述する

まず、ChatGPTのバージョンをgpt-3.5-turboからgpt-4にアップデートすることで、より高度なレスポンスが得られるようにしました。これを試した時点で、欲しい情報が入った状態で返ってきたため、一応課題は解決できましたが、gpt-3.5-turboに比べると一回のリクエストで約20倍コストがかかるようで、一回一回のリクエストの重みを感じながら使いました(笑)バージョンを落としてでも望ましい回答が得られる方がいいと思い、他の方法も試しました。

次は、温度(temperature)プロパティの調整をすると、回答の多様性をコントロールできるというものです。値が低ければ低いほどより確実な答えが返り、高いほどより創造的な回答が返ってきます。

実際に試してみました! 今回は使って欲しい食材を「卵、ゴーヤ、キウイ、ほうれん草」にして、chatGPTに献立を提案してもらいました。

バージョン3.5 値が低いとき(temperature = 0.1)

temperature = 0.1のときのレシピ提案画面

値が高いとき(temperature = 0.9)

temperature = 0.9のときのレシピ提案画面

値が低いときは味が想像できるレシピを提案してくれましたが、値が高くなると「キウイとゴーヤのサラダ」という何とも言えないレシピを提案してくれました(笑)

さらに、descriptionを充実させることで、より細かい情報を得ることができるようにしました。これは3つの中で単純でありながら効果が高い方法だと感じました。

さらにやってみたいこと

画像生成で、料理の画像を表示してみたいです。 世に出ている料理検索アプリは画像や動画が基本的にはあります。画像や動画があると、視覚的に分かりやすくなり、ユーザーの作るモチベーションに繋がると思うので、画像も表示できたらもっと使いやすくなるかなと思いました。

まとめ(所感)

 Flutterでの開発は初めてだったので、APIから受け取ったデータや関数の定義の型などを意識しなければならないのが新鮮でした。また、今までの開発ではクラスやインスタンスを利用したデータの受け渡しや操作を行わず一つのファイルで変数を多用するという開発をしていたため、データの構造が見やすくなり、結果的に実装しやすくなるということが分かりました。  chatGPTは、基本的な知識しかなかったため、概念的な理解を理解するのがそもそも難しいなと感じました。今回動くものは作れましたが、完全に理解できたとは言えないので、これからも少しずつ勉強していきたいです。

参考資料

Tempertureについて
platform.openai.com

Function Callingについて

rakuraku-engineer.com

Kingsoftは随時新メンバーを募集中ですので、一緒に働ける方をお待ちしております!

f:id:wowtech-dev:20190313163146j:plain

iOS版WowTalkアプリへWidgetを追加した時の学びをまとめておく

明けましておめでとうございます。ワウテックアプリ開発チームの岡野です。

久しぶりの投稿となりましたが、今回は今度リリース予定のiOS11.9.1で利用可能になったwidget機能について、いくつかでた課題とその対応方法について後学のため整理しておきたいと思います。

先日Androidのwidget機能についてもまとめておりますので、そちらもご確認いただければと思います。

engineer.wowtech.co.jp

Widget機能について

Widget機能はiOS14から搭載された機能で、ホーム画面にアプリの特定の機能のショートカットを追加する機能です。お天気アプリやニュースアプリ、株などのWidgetを使ってる方も多いのではないでしょうか。僕自身もカレンダーアプリや天気アプリやバッテリー残量を表示するWidgetは日頃使っています。

WowTalkのWidget機能


iOS版WowTalkの11.9.1から、日頃連絡を取るメンバーのチャットルームへ遷移するためのWidgetを実装しました。

デフォルトで現在ログインしている自分のアカウントが選ばれた状態になってますが、編集メニューからメンバーリストにいる他のメンバーを選択することも可能です。Widgetは複数登録することもできるので、ホーム画面から簡単にメンバーに連絡をとることができるようになります。

また今回はシンプルにこの機能だけですが、今後は通知バッジを表示したり共有、タスク、日報など各種機能の情報を表示するなども検討できればと思います。

Widget機能実装で困ったこと

ここからは今回WowTalkでWidget機能を実装していく上で生じた課題と対応方法について共有したいと思います。

既存のクラスが使えない

問題

現在のWowTalkのクラス間の関係は複雑になっており、様々なクラスから参照されるユーティリティクラスが存在します。またこのクラスが逆に他のモデルクラスも参照することから内部的には不要な参照を含むクラス間の関係性が形成されています。

これは今後解消を要する課題であると同時に、今回Widgetを実装する上での大きな課題となりました。

例えば、WidgetExtensionのTargetでWowTalk内のメンバーを表すBuddyクラスを利用しようとしたとき、XcodeからBuddy.mのTargetMembershipにWidgetExtensionを追加する必要があります。

このときTargetMembershipに追加する必要のあるクラスはBuddyを取得するためのDatabase、およびBuddyが参照してるクラスも含む必要があります。しかしながらDatabaseやBuddyから参照してるユーティリティクラス(Utility.h)が今度は別の多くのクラスを参照している状況から、本来は不要なクラスを全部Target Membershipに追加する必要が出てきてしまいました。

「だったら全部追加すればいいだろう」と言う人もいるかもしれませんがその数が膨大だったため追加漏れがないかビルドの都度チェックしたり、ARC(自動でメモリを解放する仕組み)を利用していない古いObjective-Cファイルを追加したときに発生したコンパイルフラグの互換性エラーを解消する必要が出てきたりと、過剰な労力が発生したため既存のクラスの利用は避けることにしました。

複雑に絡み合うカルマ

対応

上記の理由からBuddyを利用することは諦め、新しくWidgetで表示するメンバー情報のみを管理するBuddyWrapperクラスを作りました。他のクラスとの参照関係を最低限にすることでWidgetExtensionがビルド時にCompileする必要があるクラスを低減しました。

また他の利点として、本来Buddyクラスで保持していたプロパティの中にはWidgetでは使わないものもあり、その分のコードを削除することでクラスとしてもスッキリすることができました。

また他にも部門情報を保存するGroupRoomInfoの代わりにDepartmentInfoWrapperクラスを、UserDefaultへの値の設定や取得をするクラスの代わりにGroupDefaultクラスを作成するなど、Widgetに必要な最低限の依存関係で構築されたクラスを作成・利用することでWidgetExtension内部のクラス関係をスッキリさせることができました。

クラス間の依存関係を必要最低限にする

Widgetの表示がグレーアウトする

問題

Widgetがグレーアウトして閲覧・編集ができない

Widgetの開発を進めている途中で、ビルドしたWidgetがグレーアウトする現象が発生するようになりました。この問題は自分で開発をしている時には気づかなかったのですが、テスターから指摘を受けWowTalkアプリケーションの再インストールを行ったり検証で使っていた端末以外の端末などで再現することがわかり、何が原因なのかがなかなかわからない問題の1つでした。

対応

特定のcommitから問題が起きていることはわかったので、一度問題が起きていない時点のcommitからブランチを作り、調査を進めたところ、Widgetに表示する文言の多言語対応(localize)を行った際にIntentdefinitionファイルをBaseではなくEnglishにしてしまっていたことが原因でした。

正しい状態。右側のLocalizationを見るとintentdefinitionの設定がBaseになっており、サポートする言語にチェックが入る。

間違った状態。Baseにチェックがない。

Base(もしくはBase .lproj)とは、StoryboardやXibのファイルを多言語対応させる際に生成されるもので、大昔の書き方ではStoryboard内のテキストを多言語対応する際はlocalizableの設定ファイル(上のプロジェクト画像左のproject ツリー上でオレンジのアイコンで表されるもの)ではなくStoryBoardやXibファイルごとに設定ファイルを定義する必要がありました。

Widget Extensionとは異なり、iOSプロジェクトではProject > info > Use Base Internationaliztionを有効にすることでStoryBoardやXibファイルと言語設定ファイルを切り離すことができるようになりましたが、intentdefinitionなどにはこの設定がないため、従来通りBaseにチェックを入れて多言語対応をする必要があります。(参考リンク参照)

Note

Xcode adds the Base and the development language to the localization table by default. Use the Base localization for resources that support string substitution at runtime, such as storyboard, XIB, and Siri intent definition files.

データの共有方法

問題

WowTalkアプリケーションではアプリ内で利用するユーザのデータはローカルのDBに保存されていますが、Widget ExtensionからはこのDBファイルに直接アクセスすることはできず、Widgetの設定画面で利用したいユーザの情報を本体アプリのターゲットから直接共有できませんでした。

対応

iOSのapp Groupsの仕組みを利用し、アプリケーションがバックグランドになるタイミングで、DBファイルをapp Groups用のディレクトリにコピーし、Widget ExtensionからコピーされたDBファイルを参照、データを取得することで情報の共有を行いました。

この手法の利用シーンは幅広く、NotificationServiceなどの Extensionでも本体アプリのデータを利用したい時などに利用できます。

またDB以外にもWidgetに表示するユーザのアイコンなども同様の仕組みを利用して本体と同期できるようにしています。

@implementation Database(AppGroup)
//アプリがバックグランドに切り替わる時に呼ばれる処理
+(void)copyDBFile {
    //copyされるDBファイルはユーザごとの切り分けれるようユーザ固有のIDにする
    NSString* wowtalkid = [[NSUserDefaults standardUserDefaults] objectForKey:@"wowtalk_id"];
    NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                                       NSUserDomainMask,
                                                                       YES);
    NSString *directory = [documentDirectories objectAtIndex:0];
    NSString * dbPath = [directory stringByAppendingPathComponent:[NSString stringWithFormat:@".%@",wowtalkid]];
    NSFileManager *manager = [NSFileManager defaultManager];
    if ([manager fileExistsAtPath:dbPath]) {
    //同一のAppGroupsに設定されたProject, Extensionで共有可能なディレクトリパスを取得
        NSURL *url = [manager containerURLForSecurityApplicationGroupIdentifier:@"//app groupsのIDを設定"];
        NSData *db = [manager contentsAtPath:dbPath];
        NSString *path = [url.path stringByAppendingPathComponent:[NSString stringWithFormat:@".%@", wowtalkid]];
        [db writeToFile:path atomically:YES];
        NSDictionary *attr = [manager attributesOfItemAtPath:url.path error:nil];
        
        NSLog(@"copied db size :%llu",attr.fileSize);
    }
}

@end

まとめ

いかがだったでしょうか。

1つ目の「既存クラスが使えない」という問題は当社特有の問題といえばそうですが、どこのサービスでも起こりうる問題だと思います。特に歴史が長いサービスなどで構成の見直しや技術負債の精算が行われていない状況では起きやすいのかなとも思いますので回避するテクニックの1つとして参考になれば幸いです。

また2つ目の「Widgetがグレーアウトする問題」というのも、「iOS14の初期で起きてる問題だからOSをアップデートしろ」や「端末を再起動してみろ」といった回答は見かけるのですが具体的な原因となりうるものについて言及した記事は少ないので同じ問題を抱えてる方の一助になれば幸いです。

今回のWidget開発を通じて改めてiOS開発の奥深さを感じましたが、今後はユーザの声を反映してWidgetをより便利にしていければと思います。

参考

Adding support for languages and regions
https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions

Internationalizing the User Interface https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/InternationalizingYourUserInterface/InternationalizingYourUserInterface.html#//apple_ref/doc/uid/10000171i-CH3-SW2

iOSアプリの多言語対応について
https://techblog.zozo.com/entry/ios-localization

WowTechは随時新メンバーを募集中ですので、一緒に働ける方をお待ちしております!

f:id:wowtech-dev:20190313163146j:plain

初めてFlutter開発に挑戦してわかったこと・困ったこと

こんにちは!アプリ開発チームでAndroidの開発を担当しているラジタです。

今回はFlutterフレームワークを使って、WowTalkのモバイル版のサンクス機能の開発を行いました。最近Flutterでの開発アプリや機能が多いので、この記事では、Flutterの開発について自分が困った点や、気に入った点を紹介したいと思います。

Flutterの説明

Flutterとは、2018年にGoogleが発表したマルチプラットフォームアプリ用のオープンソース(ソースコードが一般公開されている)型のフレームワークです。この枠組みの「Hot Reload」という、書いたコードを即座にアプリの挙動に反映できる機能を使うことで、より簡単かつ迅速に開発を進めることができるようになります。Flutterのフレームワークには、Googleが2011年に発表したDartと呼ばれるアプリ開発言語が使われています。

Dart言語説明

Dartとは、サーバー、モバイル、デスクトップ、ウェブアプリケーションを構築できる高水準のプログラミング言語です。Dartと他のプログラミング言語の違いは、Pubと呼ばれる独自のパッケージマネージャーが使われていることです。開発者はこれらのパッケージを使用して、DartおよびFlutterアプリを構築できます。

Flutterを使うメリット

  1. 開発速度の向上と開発コストの削減
    Native開発との大きな違いは、クロスプラットフォーム開発がある点です。これにより1つのソースコードでiOSとAndroidという複数のWowTalkアプリの機能の開発ができるようになるため、開発の速度が上がり、プラットフォーム間で実装しないといけないコードの総量を削減することができます。
  2. 保守と運用コストの削減
    WowTalkのサンクス以外の機能に関して、Androidアプリは、Androidアプリエンジニア(自分)が、iOSアプリはiOSアプリエンジニアが保守・運用を担当します。稼働が増える分、それに合わせて、保守と運用コストも増えます。クロスプラットフォーム(Flutter)開発の場合は、一つのソースコードでAndroidとIOS両方モバイルアプリを動かしますので、対応稼働が減り、必然的にコストも減ります。
  3. 仕様変更に強く、試験にかかる工数を抑えることができる
    WowTalk Native開発では、ソースコードの変更後は、変更点適応のためにモバイルアプリの再起動(再ビルド)をする必要があります。しかし、Flutterにはホットリロード(Hot Reload)と言う機能があり、開発中にcmd+sを押すと、変更点をすぐにリロード(更新)することができます。

Flutterを使うデメリット

  1. 端末独自のOSの影響を受けてしまう
    Flutterの大きなメリットは、一つのコードでWowTalk IOSとAndroidモバイルアプリ開発ができることを挙げましたが、OSごとに搭載されている機能(カメラ機能、位置情報機能など)に関しては、個別でソースコードの更新をする必要があります。また、iOSやAndroidのOSアップデートで追加された新しい機能をFlutterがサポートするまでに時間がかかる場合があります。
  2. ネイティブ開発と比べるとライブラリが少ない
    ネイティブ開発で用いられるアプリ開発言語は、(iOSアプリの場合はSwift、Androidアプリの場合はJavaなど)と比較すると少ないですが、徐々に増えてきています。
  3. ネイティブ開発の方が動作面で有利に働く場合がある
    Flutterは、マルチプラットフォームに対応していることから、保守性の面でも動作の改善は早いですが、UI側でのレンダリング、ドロップダウンスピード、テキストのフォーカスなどの面でNativeアプリと比べると、動きが良くないと感じる場合があります。

よく使うクラス

StatefulBuilderとStatelessWidget

Stateとは「状態」や「状況」という意味です。変数の値、端末内に保存しているデータやAPIで取得したデータ、入力フォーム等のUIの状態や入力内容、端末の状態などがこれにあたります。このウィジェットタイプは自分の変数を変更することで自身を再ビルドすることができ、表示内容を自動的に変更できます。

StatefulBuilderとは、StatelessWidgetをStatefulWidgetとして扱うためのウィジェットです。

今回この機能のモバイル版の開発にあたり、交換時に表示するダイアログは、StatefullBuilderを使って作成しました。

return AlertDialog(
          title: Text(Localized.of(context).redeem_point_exchange_title),
01          content: StatefulBuilder(
              builder: (BuildContext context, StateSetter setState) {
            return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
              Padding(
                  padding: const EdgeInsets.all(10),
                  child: Text(
                    Localized.of(context).redeem_point_exchange_sub_title,
                    style: const TextStyle(color: Colors.grey, fontSize: 12),
                  )),
              Padding(
                padding: const EdgeInsets.all(0),
                child: DropdownButtonHideUnderline(
                  child: DropdownButton2(
                    isExpanded: true,
......
                    value: _selectedPoint,
                    onChanged: (value) {
                      _selectedPoint = value as String;
                      _selectedItem = int.parse(_selectedPoint!);
                      exRate = (_selectedItem/exchangeRate).toString();
 02                     setState((){
                        requiredPoint = MessageUtil.format(Localized.of(context).redeem_point_exchange_required_point, exRate);
                        requiredPointDescription = MessageUtil.highlightText(requiredPoint,exRate, const TextStyle(color: Colors.grey, fontSize: 12), const TextStyle(color: myColors.point_highlight));
                      });
                    },

                ),
              ),
              Padding(
                  padding: const EdgeInsets.all(10),
03                  child: requiredPointDescription),
              Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
......
  1. StatefulBuilderウィジェットを追加しています。
  2. DropDownのonChangeを行う際は、必要なポイントを計算し、setStateを使ってUI更新依頼を行います。
  3. UIに設定されているTextは更新されます。

以下はUIが更新される画面の例です。

ページウィジェット (StatelessWidget)とは、Stateの状態を持たない静的な(static)ウィジェットのことです。ウィジェットで使用する変数は親ウィジェットから渡された値を使用することしかできません。値の変更はウィジェット内でできません。

WowTalkのサンクス機能では、ユーザが獲得しているポイントを表示するヘッダー部分をStatelessWidgetで実装しました。

このヘッダー部分のポイント値は、APIから返ってくる値を表示する設定です。サンクステンプレートやギフトを交換した際に、APIから返ってきた値は更新されます。

ページ移動 (Navigator)

Flutterでの宣言的なUI構築で画面遷移の方法にNavigatorを使います。今回の実装でNavigator.pushとNavigator.popを使いました。

child: GestureDetector(
              onTap: () {
                Navigator.push<void>(context,
                    MaterialPageRoute(builder: (context) => GiftRecordView()));
              },

Navigator.pushを使うと、指定した画面(widget!)へ遷移します。

void pressOk(BuildContext context, String _selectedItem) {
    for (var gift in feePointGiftList) {
      if (gift.requiredPoint.toString() == _selectedItem) {
        exchangePointHandler(gift.giftId.toString());
        Navigator.pop(context);
      }
    }
  }

Navigator.popの場合、元の画面に戻ります。

大変だった点

アラートボックスの種類が多かった

ポイント交換ダイアログ画面のDropDownを表示する必要があり、調べてみると、複数の開発元から作られたライブラリーが見つかりました。最初にawesome_dropdownライブラリーを使って開発を行いました。動作確認した際に、ドロップダウンを開いてキャンセルで戻った場合、UIが残ってしまうという不具合が発生しました。また、IOSでタップしても反応しない不具合も発生しました。Googleで解決方法を調べても見つからなかったので今回はdropdown_button2ライブラリーを使っています。

大変だった実装

APIで取得したデータをViewに反映させるまでの流れがNativeと異なっていた

今回のプロジェクトでAPIからのデータ取得は、index.dartファイルをentry pointにしてViewに反映させるまでの流れになっていました。

AndroidのNative開発では、直接対象の画面からAPIを呼んでデータ取得することになっているので流れに関して悩みました。

WowTalkのNative側の実装では、画面に表示したいデータがある場合、ActivityからAPIへリクエストするためのクラス(WebIf)へリクエストを送り、返ってきたレスポンスをViewに渡して表示していますが、Flutterの実装では画面のルートViewにあたるindex.dartで子ビューで利用されるProviderを登録し、Providerが保持するBlocを使ってAPIへのリクエストと親ウィジェットから子ウィジェットへのデータの受け渡しを実現しています。

リンクをブラウザで開く方法

今回の対応で交換履歴からリンクを開く必要があったので、リンクをブラウザで開くコードを調べました。

まず①のコードでは上手くいきませんでした。

①上手くいかなかったコード

Future<void> _launchURL(String strUrl) async {
  final url = Uri.parse(strUrl);
  if (await canLaunchUrl(url)) {
    await launchUrl(url);
  }
}

②上手くできたコード

Future<void> _launchURL(String strUrl) async {
    if (await canLaunch(strUrl)) {
      await launch(strUrl);
    }
  }

原因を調べてみると、「launchUrl」メソッドとURLが有効か否かチェックする「canLaunchUrl」が、バージョン6.1.0から変更されたメソッドになっていました。今回使ってるのは、6.0.5バージョンなので②の書き方が正しかったです。

今回使ったライブラリーは「url_launcher | Flutter Package」です。

Debugについて

Flutterの開発中にDebugで実装確認する必要があり、BreakPointを設定し、手元の端末でDebugモードで動かしてみました。するとBreakポイントに止まりませんでした。

チーム内で相談したり、Googleで調べてみると、Emulatorだと上手くいくということが分かったので、Emulatorで試してみると、Debugが上手くできました。

改善する必要がある点としては、今回サンクス機能以外はNativeで開発しているので、WowTalkアプリと一緒にDebugができなくなっています。不具合対応やバージョンアップをするとまたFlutterのサンクスモジュールで別に対応してNative(wowtalk)アプリに混ぜる必要があります。

まとめ

Flutterを使って初めて開発しましたが、Flutterはすごく便利なフレームワークだと思いました。これからもFlutterを使って新しい機能開発をしたいと思います。

ワウテックには、多くの学びを得る機会と挑戦させてもらえる環境があります。
随時新メンバーを大募集中ですので、一緒に働ける方をお待ちしております!

f:id:wowtech-dev:20190313163146j:plain

参考

  1. URLを開くために使うライブラリー
    url_launcher
  2. カスタマイズができるドロップダウンライブラリー
    dropdown_button2awesome_dropdown

サンクス新機能の開発を通して新しく勉強になったこと

こんにちは!新卒2期生として7月からWeb開発チームに配属されました。河原です(^^)
今回の記事が初投稿です!!入社前に読んでいたブログをまさか自分が執筆する側になるとは思っていなかったので、今すごく嬉しいです。よろしくお願いいたします!

今回WowTalkのサンクス新機能の開発に携わらせていただいたので、そこで勉強になったことを初学者の視点で、まとめていきたいと思います。

はじめに

開発に携わったサンクス新機能ですが、実はどんな機能なのかまだ発表できません...!ですので、今回は代わりにWowTalkのサンクス機能についてご紹介します!

WowTalkのサンクス機能について

WowTalkのサンクス機能は主に3つの機能から成り立っています。

  1. レター機能
    日頃のちょっとした場面や大事な場面で仲間にありがとうを送る仕組みです。レターを読むと何だかほっこりします。

  2. サンクススコア機能
    WowTalk内でありがとうの気持ちがどれほど伝えられているか、自分を中心としてサンクススコアとして可視化されます。

  3. ポイント機能
    自分がありがとうを送った分だけポイントが貯まります。現在貯めたポイントは新しいレターのデザインと交換できます。すごく可愛いデザインのレターがたくさんあり、どれと交換しようか迷ってしまいます。

「ありがとう」で幸せな働き方の実現をサポートする
心あたたまる機能がこのサンクス機能です!

サンクス機能開発を通して勉強になったこと

それでは、今回サンクス機能の開発で勉強になったことを、自身の復習も兼ねてフロントエンド、API実行部分、バックエンド開発に分けて、まとめていきたいと思います!(勉強になったことがたくさんありすぎて、どれを取り上げて記事にしようか非常に迷いました(笑))

フロントエンド部分での学び

【Redux・Redux-toolkitについて】

学生時代に本当に少しだけReactをかじっただけの私。stateの管理といえば、useStateuseStateはとっても便利!という感覚でしたが、いざ既存コードを見ると、Redux-toolkitというものが。なんだこれは。いきなりパニック。

useStateでもこんなに便利なものがあるのか〜と感動していましたが、Redux-toolkitは更にデータを管理するのに便利な仕組みが盛りだくさんでした!

【ReduxとRedux-toolkitの違い】

弊社のサンクス機能の開発で使われているのはRedux-toolkitです。これはReduxをより簡単に扱えるようにしたものです。

【useStateを用いたデータ操作のイメージ】

useStatestateを簡単に管理することができる機能です。statepropsと異なり、自由にデータを書き換えることができます。useStateの状態管理はそのコンポーネント内で完結するという特徴があります。

graph TB
    id1((ItemModal<br/><br/>itemModalVisible<br/>true/false))
    id2((MyMenu<br/><br/>LoginCount<br/>1,2...))

【propsを用いたデータの受け渡しのイメージ】

useStateは同じコンポーネント内のみでデータを保持するため、コンポーネント間でのデータの受け渡しではpropsを用います。propsの特徴として、親コンポーネントから子コンポーネントに渡される値であり、子コンポーネントから親コンポーネントの値を直接操作することはできないといった制約があります。

graph TB
    A("App")--props-->B("ShopMenu")--props-->D("ItemModal")
    A("App")--props-->C("MyMenu")--props-->E("PointModal")

この図の場合、MyMenuからItemModalへのデータの受け渡しや、ItemModalからPointModalへといった、親子関係のないデータ同士の受け渡しはできません。

またpropsは親コンポーネントから値を受け取っているだけなので、子コンポーネントで値を変更して、親コンポーネントに値を返す...といったことはできません。

graph BT
D("ItemModal")--X NG X-->B("ShopMenu")
E("PointModal")--X NG X-->C("MyMenu")

【Reduxを用いたデータの受け渡しのイメージ】

しかし、Reduxはコンポーネントの外で状態管理をするため、どのコンポーネントからでもデータがアクセスでき、簡単にデータを管理することができます!!コンポーネント間の関係性を考えなくて良いのはとても便利でした。

【Reduxを用いたデータの受け渡しのイメージ】

graph TB
    A("Redux")--useSelecter-->B("ShopMenu")-->A("Redux")
    A("Redux")-->C("MyMenu")-->A("Redux")
    A("Redux")-->D("ItemModal")-->A("Redux")
    A("Redux")-->E("PointModal")--useDispatch-->A("Redux")

今回のサンクス新機能での開発では、フロントに表示させるデータを親子関係のないコンポーネント間で変更させるという作業が多かったのですが、Redux-toolkitを利用したことで、コンポーネントの繋がりを気にすることなく、どのコンポーネントからでもuseSelecter()useDispatch()を使ってアクセスできました!これは自分の中で革命だったと思います。

Reduxを組み合わせることで、Reactがまたさらに使いやすくなるのですね...!!すごいです!

★Redux・Redux-toolkitの使い方を執筆するにあたり以下の記事を参考にさせていただきました。ありがとうございました。

API実行部分開発での学び

フロント部分の開発を進め、新しい画面ができていくことに喜んでいた私ですが、その後先輩に作成していただいたAPIを使って、サーバーからデータを取得することを先輩に勧められました。

Fetch API非同期処理Promise?なんだそれは???わからない言葉だらけで、またまたパニックになりました(笑)色々と試行錯誤を繰り返し、先輩に作っていただいたAPIから初めてデータを取ることができた、あの何とも言えない感動はすごかったです...!

ですのでこの項ではFetch APIの使い方についてまとめ直し、得た知識を改めて確認しようと思います。

【Promiseについて】

ではここで、Fetch APIの説明で出てくるPromiseについて復習しておきます。Promiseは、非同期処理の最終的な完了、もしくは失敗を表すオブジェクトです!

【そもそも非同期処理とは何か?】

非同期処理とは、処理結果を待たずして次の処理に進める処理のことです。
もし仮にコードの途中に非常に処理に時間のかかるコードがあったとします。順番通りに行う同期処理では、その処理が終わるのを待たなくてはいけないため時間がかかります。

しかし、非同期処理はその処理が終わる前に次の処理を実行できるため、時間短縮につながりユーザビリティ向上が期待できます。

【Promiseの引数について】

Promiseresolveと、rejectの2つの関数を引数に取ります。 - resolve:処理が成功した時のメッセージを表す - reject:処理が失敗した時のメッセージを表す

その後、thenを使ってコールバック処理を実行します。

【then(),catch()について】

  • then():処理が成功した場合(resolve)の処理を実行する。
  • catch():処理が失敗した場合(reject)の場合の処理を実行する。何だか、try...catch文みたいです!

ここまでの流れを図にしてみると...

graph LR
    A("Promise")--"成功"-->B("resolve(data)")--"dataを渡せる"-->D(".then(data)")
    A("Promise")--"失敗"-->C("reject(error)")--"errorを渡せる"-->E(".catch(error)")

ここまでの流れをコードで見てみると...

let getItemsInfo = new Promise((resolve, reject) => {
    //このコードは3000ミリ秒後、つまり3秒後に実行される
    setTimeout(function(){
        const data = [
            {name: "炎の剣", price:500},
            {name: "氷の剣", price:500},
            {name: "雷の剣", price:500},
            {name: "鉄の鎧", price:1000},
            {name: "薬草", price:50},
            {name: "すごい薬草", price:100},
        ]
        //コメントを外すとエラー処理が実行される
        // if(data.length > 5){
        //     //今回手動でエラーを投げているため、rejectを使わなくても自動的に
        //     // catchに飛ぶのでこのような表記になっている。基本的にはreject()を記載すること
        //     throw "アイテムの数が多すぎるよ!持ちきれない!";
        // }
        resolve(data); 
    }, 3000);
}).then((data) => {
    console.log(data);
    //【成功したら】dataの中身が出力
}).catch((err)=> {
    console.log(err); 
    //【失敗したら】Uncaught アイテムの数が多すぎるよ!持ちきれない!と出力
});

//
console.log("先に出力されるよ");

★Promiseの使い方を執筆するにあたりこちらの記事を参考にさせていただきました。ありがとうございました。

【Fetch APIについて】

Fetchの意味を和訳すると、読み込む、とってくるという意味がヒットします。Fetch APIはその意味の通り、外部のAPIにリクエストを送信し、データを取得することができるインターフェイスです。

では早速、APIからデータを取得してみましょう! まずはリクエストを発行します。

//先輩に作っていただいたAPIからデータをとるぞー!
fetch("https://〇〇〇.sample.php");

み...見えない...!!(´·ω·`)ショボーン

Promiseが結果で出力されているため、中身が見えません。 ですので後ろにthenを繋げて必要な処理を書いていきましょう! エラー処理もされるようにcatchも書いて...と。

fetch("https://〇〇〇.sample.php")
    //  Response オブジェクトから JSON の本文の内容を抽出する
    .then(response => {
        return response.json();
    })
    // Promiseのデータの中身を取り出す
    .then(data => {
        console.log(data);
        //おまけ
        for(const element of data){
            console.log(`${element.name}${element.age}歳です!`);
        }
    })
    // エラーを検出したら、catchに処理が入る
    .catch(error => {
        console.log(error);
    });

先輩のAPIから無事dataが取れました!わーい!!(^○^) これがFetch APIでのデータ取得の流れです!

★Fetch APIについて執筆するにあたりこちらの記事を参考にさせていただきました。ありがとうございました。

バックエンド部分での学び

当初の目標としてはフロントエンドのみの開発だったのですが、せっかくAPIからデータを取得できたので、今度はAPIの開発にもチャレンジしてみないかと先輩からご提案をいただき、なんと今回、バックエンドの開発にも少しだけ携わらせていただきました!(うれしい!)

フロント以外の部分はほとんど触れたことがなかったので、わからないことだらけでしたが、こうしてアウトプットする機会をいただけたので、少しでも多く学びとして残せるよう、頑張って執筆してみようと思います!チャレンジです!

バックエンドはnode.jsAWS Lambda関数を書きました!

【エラー処理について】

念願の技術開発部での開発の初仕事ということもあり、舞い上がっていた私。とにかく何としてもまずは動くようにということにばかり意識を取られてしまい、最初先輩にコードレビューをいただいた際にエラー処理に不十分な部分がたくさんあるとご指摘いただきました。まさしく私の性格を体現したような猪突猛進コードだったと思います...(笑)

ここからは開発途中に特に躓いた部分を取り上げます。

【アクセス途中でのエラー】

例えばデータベースに接続してItemdataを取得するとします。

Itemdataのイメージ】

itemId name price
1 炎の剣 500
2 氷の剣 500
3 雷の剣 500
4 鉄の鎧 1000 
5 薬草 50
6 すごい薬草 100

このとき、データベースにアクセスする際に、エラーが起きてしまい正しくデータが取れないかもしれません。 もしこの時にエラーメッセージを出さないと、なぜアプリが動かないのかがわからないため、お客様が混乱してしまいます。

const getItemdataHandler = () => {
    // getItemdata()でItemdataを取得できるとする
    const res = await getItemdata();
    if (res !== false) {
        return createResponse(res);
    } else {
        //resの中にきちんと値が入っていない場合はエラーを出す
        return createErrorResponse("getItemdataHandler err");
    }
};

これでもし通信が失敗しても、エラーメッセージが表示されるようになりました!

【予期していないデータが入る可能性】

ここで先ほどのItemdataですが、このように当初の予定とは異なったデータが入ってしまう可能性もあります。

【予期しない値が入ったItemdataのイメージ】

itemId name price
1 炎の剣 500
2 氷の剣 500
3 雷の剣 500
4 鉄の鎧 1000 
5 薬草 undefined
6 すごい薬草 100

薬草のpriceundefinedになってしまっています。 この状態でもしitemを購入しようとしてしまうと...

let yourMoney = 1000;
const buy = (id) => {
    const item = Itemdata.find(item => item.itemId === id);
    console.log(`${item.name}を購入した!`);
    yourMoney = yourMoney - item.price 
    console.log(`今の所持金は${yourMoney}円です`);
    //薬草を購入した!
    //今の所持金はNaN円です と出力
}

buy(5);

本来数値が入って欲しい部分にNaNが入ってしまいました...!!(T T) このまま処理を進めてしまうと、さまざまなエラーが起きてしまいます。

また、今回はundefinedでしたが、もしnullが入った状態でこのコードを走らせてしまうと

薬草を購入した!
今の所持金は1000円です

と表示されてしまいます。今度はタダで買えてしまう...!これはまずいです(^^;

ですので、適切な値が入っているかチェックするコードを追加します。

let yourMoney = 1000;

const buy = (id) => {
    const item = Itemdata.find(item => item.itemId === id);
    if(item.price === undefined || item.price === null){
        console.log("priceの値が不適切です!");
    }else{
        console.log(`${item.name}を購入した!`);
         yourMoney = yourMoney - item.price 
        console.log(`今の所持金は${yourMoney}円です`);
    }   
}

buy(5);
// priceの値が不適切です!と出力

【そもそもデータがない可能性】

Itemdataそのものが空の場合

Itemdataの中にデータがあるものだと仮定してコードを書いていましたが、Itemdataに、もしもデータが入っていなかった場合にもエラーが起きてしまいます。

let yourMoney = 1000;

const buy = (id) => {
    // lengthで空かどうかを判定、エラー処理を追加。
    if(Itemdata.length === 0){
        console.log("Itemデータがありません");
    }else{
        const item = Itemdata.find(item => item.itemId === id);
        if(item.price === undefined || item.price === null){
            console.log("priceの値が不適切です!");
        }else{
            console.log(`${item.name}を購入した!`);
            yourMoney = yourMoney - item.price; 
            console.log(`今の所持金は${yourMoney}円です`);
        }   
    }
}
buy(3);

これで万が一Itemdataの中身が空でも処理は止まりませんね!よかった、よかった。ε-(´∀`*)ホッ

指定されたitemIdのデータがない場合

Itemdataのデータは6つ、つまりitemIdは6までしかありませんが、もしここで存在しないitemIdを指定した時にもエラーが起きてしまいます!(こちら先輩にご指摘いただき、気がつきました。気をつけていてもやはり見落としがありますね!勉強になります(^^)!) それでは早速エラー処理を書いていきましょう!

const buy = (id) => {
        if(Itemdata.length === 0){
            console.log("Itemデータがありません");
        }else{
            const item = Itemdata.find(item => item.itemId === id);
            //指定されていないidを指定した場合、itemの中身はundefinedになるのでエラー処理を追加
            if(item === undefined){
                console.log("指定したデータがありません");
            }else{
                if(item.price === undefined || item.price === null){
                    console.log("priceの値が不適切です!");
                }else{
                    console.log(`${item.name}を購入した!`);
                    yourMoney = yourMoney - item.price; 
                    console.log(`今の所持金は${yourMoney}円です`);
                }   
            } 
        }
    }

buy(7);
// 範囲外のitemIdを指定した場合は指定したデータがありませんと表示。

できました!と思いきやあれ...?なんだかコードが読みにくくなってきたような...?(゜∀゜;)コンナハズデハ
それもそのはず。ネストが深くなってしまったのです...!ガード節を使い修正します。

コードを修正!
const buy = (id) => {
    if(Itemdata.length === 0){
        console.log("Itemデータがありません");
        return;
    }

    const item = Itemdata.find(item => item.itemId === id);

    if(item === undefined){
        console.log("指定したデータがありません");
        return;
    }

    if(item.price === undefined || item.price === null){
        console.log("priceの値が不適切です!");
        return;
    }

    console.log(`${item.name}を購入した!`);
    yourMoney = yourMoney - item.price; 
    console.log(`今の所持金は${yourMoney}円です`);
}

buy(4);

先ほどのコードよりもすっきりしたかなと思います!よかったです!
お客様にご利用していただくサービスだからこそ、処理の止まらない、エラーが起きた際はエラーが発生したのだとわかるようなコードを書くことの大切さを学びました。

終わりに

最後までお読みいただきありがとうございました! たくさん挑戦できる環境をいただけ、すごく嬉しかったです! いつもたくさんご指導いただきありがとうございます!もっともっと知識を吸収して、早く一人前のエンジニアになるのが目標です。これからも頑張ります!(^^)

プログラミング未経験だった新卒が語る!これまでとこれから

こんにちは!新卒2期生として7月からアプリ開発チームに配属されました、M.I.です!

本日は、入社からこれまでに体験したことや、プログラミング未経験の身でどのように開発へ携わっているのかをお話したいと思います。
この記事が、ワウテックに興味のある皆さんにとって働くイメージを持つ一助になると嬉しいです✨

 

学生時代のお話

前述の通り、私は現在技術開発部のアプリ開発チームに所属しています。が!学生時代からプログラミングに慣れ親しんでいたかというと、そうではありませんでした。

元々文系の学部に所属していて、プログラミング自体もProgateという学習アプリに触れたことがある程度でした。
そんな状態ながらも、研修期間を経て技術開発部に決まったこれまでをお話したいと思います!

 

新卒研修期間について

ワウテックでは、新卒向けにジョブローテーションという研修制度があり、私たちは約2ヶ月かけて全部署を回りました。

私は一週間技術開発部にアサインされ、製品のテストや初歩的なUI設計、サイトの構築などを行いました。

わからないことばかりでしたが、どんな疑問でも皆さん丁寧に答えてくださり、開発の面白さとチームの温かさを知る貴重な機会になりました。

 

本配属後について(自作アプリ編)

7月から本配属となり、私がまず取り組んだのは「自作アプリの開発」でした。

とにもかくにも知識がまっさらな状態なので、ひとまずアプリの作成を通して開発の基礎知識を身につけよう!💪と始めた取り組みでした。

 

概要をざっくりご説明すると、「映画の聖地といわれている場所の情報をまとめたアプリ」です!
筆者はいわゆる「聖地巡礼」が好きなんですが、既存のアプリだと自身のニーズを満たせていませんでした。

ないなら作ってしまおう!ということで実際の完成画面がこちら↓(著作権の都合上画像はぼかしています)

 

また、アプリを作る中で学んだことや楽しかったこと、大変だったことをピックアップしてみました!

学んだこと

✔️ テーブルビューの使い方
 →データをリスト状に表示する方法のことです。

✔️ ユーザーデフォルトの使い方
 →ユーザーが入力した情報を永続的に保持できます。右側の画像にあるメモ欄で利用しています。

✔️ データベース、APIの基礎知識
 →表示するデータはサーバーからAPIを利用して取得し、データベースに保存する仕組みです。

 

楽しかったこと&よかったこと

▶︎ 自分のほしいと思った機能や見た目を自分の手で作れたこと(初めてアプリを動かした時の感動は忘れられません…!)

▶︎ 作業に詰まった時も、自社製品の”WowTalk”を使ってすぐ質問できるので、テレワークでも安心して開発に取り組めたこと

 

大変だったこと

▶︎ 覚えることが多く内容整理に追われていたこと

▶︎ 疑問点が出てきた時の調べ方がわからなかったこと(個人的に調べ物をするスキルはエンジニアにとってすごく大切なものだと思ってます)

 

このような紆余曲折を経て、約1ヶ月かけ自作アプリを完成させることができました!

 

本配属後について(WowDesk編)

自作アプリ作成がひと段落し、次はワウテックの自社製品である”WowDesk”の機能追加に取り組みました。

WowDeskについてざっくりご説明すると、タブレットでオフィスへの来訪者の受付対応ができるシステムのことです。このシステムをスマートスピーカーと接続すれば、来訪の通知を音声で受け取ることもできます。(詳しくはこちら↓)

www.wowdesk.jp

 

その中で私が取り組んだのは、「来訪者の音声通知をどのスマートスピーカーで鳴らすか利用者がカスタムできる」という機能です(近日リリース予定となっております!)。

利用できるスマートスピーカーが複数ある場合、来訪者の属性によってスピーカーの鳴らし分けができる、というイメージです。

実際に作った画面がこちら↓

初めて見る自社製品の既存コードに、最初は??状態でしたが、自作アプリで得た基礎知識や上司の方の助力によりなんとか完成させることができました!本当に感謝です…!✨

 

今後の目標

いかがでしたでしょうか?以上がプログラミング未経験の新卒社員が、入社してから現在に至るまで体験したことです!
本配属から約2ヶ月、自社製品に携わる機会をいただけているものの、まだまだ自分の知識不足を感じる日々です笑
今はとにかくどんどん知識を吸収して、早く一人前のエンジニアになるのが目標です!

 

ワウテックには、多くの学びを得る機会と挑戦させてもらえる環境があります。
随時新メンバーを大募集中ですので、一緒に働ける方をお待ちしております!

f:id:wowtech-dev:20190313163146j:plain

WowTalkでウィジェット開発する際のポイント

今回初めて本ブログの執筆を担当することになりました、ラジタです。

今年の初めに入社し、現在アプリ開発チームでAndroidの開発を担当しております。近年IOSやAndroidにおいて、ウィジェット機能が注目されています。ワウトークはチャットでやり取りするユーザが多いため、ホーム画面からすぐにチャットに入れる様、Androidワウトークアプリ開発の新機能として、お気に入り連絡先ウィジェットをリリースする予定です。

今回はこの開発を通じて得た知見をもとに、ウィジェット機能を実装する際のポイントについて簡単に紹介していきます。

 

目次

 ウィジェットとは

 基本情報

 Android Studio から簡単にウィジェット作りましょう

 自動に追加されるウィジェット設定確認しましょう

 ウィジェット追加する場合のクラスの役割

 困った点

 まとめ

 

ウィジェットとは

ウィジェットは、ホーム画面上に常に表示されるアプリのショートカット機能のことです。近年IOSでもAndroidでも流行してきているホーム画面のカスタマイズに不可欠な要素です。アプリの特に重要なデータや機能を「ひと目で」確認できるようにし、そうしたデータや機能にユーザーのホーム画面から直接アクセスできるようにするもの、ということができます。ユーザーはウィジェットをホーム画面のパネルからパネルへ移動したり、(サポートされていれば)好みに合わせてサイズを変更してウィジェットに表示される情報量を調整したりできます。Androidスマホにおける利便性の要となる機能といっても過言ではないでしょう。

基本情報

Androidウィジェットを作成するには、以下のものが必要です。

  • AppWidgetProviderInfo オブジェクト
  • AppWidgetProvider クラスの実装
  • ビューのレイアウト

詳細参照:https://developer.android.com/guide/topics/appwidgets?hl=ja

Android Studio から簡単にウィジェット作りましょう!!

New > Widget > AppWidget からウィジェット追加画面を開けます。

項目名 設定
Class Name ウィジェットの名前
Placement [Homescreen, Keyguard,Both] 中から選んでください。
Resizable [Both , Horizontal , Vertical , none] ウィジェットのサイズ変更できる方法の設定
Minimum Width ウィジェットの最小幅はセルカウントで追加してください
Minimum Height ウィジェットの最小高さはセルカウントで追加してください
Configuration Screen ウィジェット設定画面追加
Source Language ウィジェットに使用する言語

自動に追加されるウィジェット設定確認しましょう

ウィジェットを作成した時にウィジェット設定ファイルが追加されます。

例:project/res/xml/favorite_contact_widget_info.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="<http://schemas.android.com/apk/res/android>"
    android:description="@string/app_widget_description"
    android:initialKeyguardLayout="@layout/test"
    android:initialLayout="@layout/test"
    android:minWidth="110dp"
    android:minHeight="40dp"
    android:previewImage="@drawable/example_appwidget_preview"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen" />
項目名 説明
minWidth、minHeight サイズ。セルをdpで変換して指定
updatePeriodMillis 更新間隔をミリ秒で指定。AlarmManagerなどで自力で更新かける場合は省略可
previewImage ウィジェット選択一覧画面に表示する画像を指定。省略した場合はアプリアイコンが使用される
initialLayout 表示するレイアウトxmlを指定
configure 配置直後に起動されるActivityを指定。ウィジェット設定画面がない場合は省略可
resizeMode ホーム画面に配置されたウィジェットをロングタップすることでサイズ変更する場合に指定
widgetCategory Android 5未満ではロック画面にも配置できたが現在はホーム画面のみの指定

ウィジェット追加する場合のクラスの役割

ユーザーからウィジェット追加する際のフローになります。WidgetConfigureActivityでユーザからの追加設定をSharedPreferencesに保存しFavoriteContactWidgetクラスを呼びます。FavoriteContactWidgetで追加情報取得して、AppWidgetManagerへウィジェットの更新を依頼します

 

  説明
WidgetConfigureActivity.kt Widgetの設定管理Activity
getPendingIntent() 別のActivityから情報取得
updateWidget() AppWidgetProviderに情報渡す
Widget.kt AppWidgetProviderクラス
onUpdate() appWidgetManagerからWidgetId取得
updateAppWidget() RemoteViewを更新(WidgetUIの更新)

 

別のActivityからウィジェットアップデートを呼ぶ方法

intentにsetActionとAppWidgetManager.ACTION_APPWIDGET_UPDATEを設定し、AppWidgetManagerからidsを取得してブロードキャストでWidgetクラスに渡します。

FavoriteContactWidgetクラスのonUpdate関数でidsを受け取って各ウィジェットの新しい情報を取得しウィジェットUI更新依頼をAppWidgetManagerに渡します。

public void updateWidgets(){
    Intent intent = new Intent(this, FavoriteContactWidget.class);
    intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
    int[] ids = AppWidgetManager.getInstance(this).getAppWidgetIds(new ComponentName(this, FavoriteContactWidget.class));
    intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
    sendBroadcast(intent);
}

実装時に困った点

①ウィジェット自動更新はどのタイミングで行うのか

今回のワウトークに追加したウィジェットの情報(連絡先ユーザのプロフィル写真、名前)は更新が必要でしたので、どのタイミングで自動更新されるか困ったので詳しく調べてみました。

AppWidgetProviderInfo.xmlの中のandroid:updatePeriodMillis="86400000"でミリ秒設定しても上手く行きませんでした。どのタイミングからカウントされるのか分からず、自動更新されるタイミングがバラバラだったので、詳しく調べてみました。

調査した結果としては”実際の更新が正確に行われるとは限りません”とオフィシャルAndroid仕様書に書いてありました。

自分の中で実装を確認したところ、ウィジェット追加したタイミングから開始するケースが多かったです。

参照:https://developer.android.com/develop/ui/views/appwidgets#other-attributes

② ウィジェット情報はどこに保存すれば良いのか

複数お気に入りウィジェット追加した場合にそのウィジェットに関しての情報どこで保存した方が良いか悩んでました。

時計などのウィジェットの場合は、別に情報を保存する必要がありません。しかし、今回開発したワウトークのお気に入りの連絡先ウィジェットでは、ユーザの名前と画像を表示することと、タップした瞬間にお気に入りの連絡先のルームを開くことができる様にするための実装だったので、ユーザーのIDとWidgetIDを保存する必要がありました。

ウィジェット情報の保存方法は二通りあります。

⑴ ローカルデータベースに保存する(詳しく複数の情報を保存する)

⑵ SharedPreferencesに保存する(IDや少ない値を保存する)

ID2つほどでしたら、データベースに入れるよりも、SharedPreferencesに保存することが推奨されています。

③ ウィジェット設定からMainアプリの別のActivityを開く際、MainActivityを開けたりしまうこと

今回はウィジェット設定の画面で別のContactActivityを開いてからお気に入りコンタクトの情報を選択する方法であり、ウィジェット設定開く時はアプリ全体(MainActivity)を開けてしまう問題が発生した上で下の方向で解決できました。

ウィジェット設定から呼び出すActivityのAndroidManifest中のLaunchMode設定はandroid:launchMode="singleTask"にする必要がありました。

修正前               修正後     

               

参照:https://developer.android.com/guide/topics/manifest/activity-element

④ウィジェット追加リスト画面のサイズ

ウィジェット追加リスト画面でウィジェットの各サイズのプレビュー画像で表示する必要がありました。画像を作成する際にどのサイズで作成する知らなかったので調べてみました。

選択一覧でホーム画面に配置できるウィジェットにはサイズが記載されており、以下の画像の2x4や4x6などがそれに当たります。 これらのサイズはセルという単位で、1x1でホーム画面のアプリアイコン1個分のサイズです。

ウィジェット追加する時はウィジェットのサイズセルで設定します。ウィジェットに表示する画像のサイズ確認するために下の計算が使います。

実装時はdpで指定するため次の式でdpへ変換します。

70 x n - 30

例えば1x1のウィジェットを作成する場合はn=1で計算して40dpx40dpで指定することとなります。

まとめ

Androidアプリ開発してたんですが、ウィジェット開発は初めてで問題なく開発段階完了できたのは嬉しいです。これからもユーザ体験高くなる新しいウィジェット開発したいと思います。

ワウテックには、多くの学びを得る機会と挑戦させてもらえる環境があります。
随時新メンバーを大募集中ですので、一緒に働ける方をお待ちしております!

f:id:wowtech-dev:20190313163146j:plain