TitaniumMobile勉強記

Web系エンジニア向けのキャリアアドバイザーやってましたが現在はフリーランスで開発含めて色々やってます。技術ネタとしてはRuby/RailsとJavaScript関連(Node.js、Titanium)あたり

038-MVC的に分割しはじめました

Titanium Mobile使ってアプリ作るときに、MVC的にクラスをわけるのがどこまで効果があることなのか正直わかっていません。

ただ、下記2点を考えると、個人的には「アリ」かなぁと思っています。

  • 今つくっているアプリ以外にも、GoogleReaderとかGitHubリーダーのようなものを今後作りたいと考えているので、そちらへの応用
  • ソースの見通しが良くなる

ちょっと前からオブジェクト指向的な設計とかMVCの考え方がわかってきたというのもあって、やたらそういう考えを取り込みたい中学生のような状況というのもあって、このあたりで少し整理することにしました

それぞれのクラスの役割

作りながら考えているので、将来変更する可能性高いですが、現時点ではそれぞれのクラスの役割として以下を想定しています

Blogger:インスタンス化する際にブロガーのユーザIDを設定することで、そのブロガーのエントリ一覧を取得できたりする。Model的な位置づけ。
※名前をblogger.jsとしているが、本当はentry.jsのほうがよいかもしれず、将来的に名前変更するかも

CreateTableView:ブログのエントリ一覧情報のオブジェクトを受け取り、TableViewの生成を行う。View的な位置づけ

Controller: コントローラーとして上記のmodelとviewを下記のように管理する

  • 初期化時

JSDeferredを使って BloggerクラスのgetEntry→ 同クラスのmakeEntryList()を順番に呼び出し、エントリー一覧を生成し、entryListオブジェクトに格納

entryListオブジェクトをCreateTableViewクラスに渡し、run()にてTableViewを生成

  • イベント発火

画面スクロール時に、情報更新するという感じのアプリが、RSSリーダーtwitterクライアント等であるかと思います。

あういうのを意識して、スクロールイベントを検知してそのタイミングで次のxx件のエントリを取得する

ソースコード

JSDeferredの使い方がイマイチ理解出来てないため、Controllerは実装できていませんが、その他は現在こんな感じに出来上がりました。

//Bloggerクラス
var Blogger = function(blogger){
  this.blogger = blogger;
  this.xhr = Titanium.Network.createHTTPClient();
  this.list = [];
  this.entry_list_length = 5;
  this.pagenumber = 1;
  this.objTable = new CreateTableView();
};

Blogger.prototype = {
  getEntry:function(url){
    var _url='',self = this;
    /*
     初期化する時は引数にURLを指定せず、追加情報を更新するときには
     該当URLを引数に与えることでgetメソッドを使い回すことが可能になる
     ので、このように処理している
     */
    if(url) {
      _url = url;
    } else {
      _url =  "http://localhost:4567/api/"
	+ this.blogger
	+ "/"
	+ this.pagenumber;
    }
    Titanium.API.info('URL IS:' + _url);
    self.xhr.open('GET',_url);

    self.xhr.onload = function(){
      var entries = JSON.parse(this.responseText);

      //ここでレイアウト情報を生成するようなメソッドを呼び出せばOK?
      self.makeEntryList(entries);
    };
    self.xhr.send();

  },
  makeEntryList:function(obj){
    for(var i=0;i<this.entry_list_length + 1;i++){
      var entry = obj[i];
      if(entry){
	Titanium.API.info(entry.permalink);
      }
      this.list.push(obj[i]);
    }
    return this.list;
    // var data = this.objTable.run(this.list);
    // return this.objTable.setData(data);
  },
  close:function(){
    this.xhr.onload = null;
    this.xhr = null;
  },
  update:function(){
    var next_url =  "http://localhost:4567/api/"
	+ this.blogger
	+ "/"
	+ this.objTable.nextPage();
    Titanium.API.info(next_url);
    return this.get(next_url);
  }
};
//CreateTableViewクラス
CreateTableView = function(){
  this.entry_list_length = 5;
  this.list = [];
  this.pagenumber = 1;
  this.win = Ti.UI.currentWindow;
  this.tableView = Ti.UI.createTableView();
  this.pulling = false;
  this.reloading = false;
};
CreateTableView.prototype = {
  run:function(entryList){
    var self = this;
    var _table_view = [];
    for(var i=0;i<this.entry_list_length;i++){
	var entry =  entryList[i];
	var row = Ti.UI.createTableViewRow({
	  hasChild:true,
	  data:entry,
	  height:80
	});
	// 記事の詳細情報を表示
	row.addEventListener('click', function(e){
	  var title = e.rowData.data.title;
	  var html_body = e.rowData.data.html_body;
	  var permalink = e.rowData.data.permalink;

	  var webView =  Ti.UI.createWebView();
	  webView.html = '<html><body>' + html_body + '</body></html>';
	  self.win.add(webView);
	  Titanium.UI.currentTab.open(self.win,{animated:true});
	});

	var title = Ti.UI.createLabel(styles["titleLabel"]);
	title.text = entry.title,
	row.add(title);
	var content = Ti.UI.createLabel(styles["contentLabel"]);
	content.text = entry.html_body.replace(/<\/?[^>]+>/gi, "");
	row.add(content);
	_table_view.push(row);

    } // for loop end
    return _table_view;
  },
  setData:function(data){
    var self = this;
    var border = Ti.UI.createView(styles["border"]);
    var tableHeader = Ti.UI.createView(styles["tableHeader"]);
    var arrow = Ti.UI.createView(styles["arrow"]);
    var statusLabel = Ti.UI.createLabel(styles["statusLabel"]);
    var lastUpdatedLabel = Ti.UI.createLabel(styles["lastUpdatedLabel"]);
    tableHeader.add(border);
    tableHeader.add(arrow);
    tableHeader.add(statusLabel);
    tableHeader.add(lastUpdatedLabel);

    self.tableView.addEventListener('scroll',function(e){
      var offset = e.contentOffset.y;
      if (offset <= -65.0 && !self.pulling) {
	var t = Titanium.UI.create2DMatrix();
	t = t.rotate(-180);
	self.pulling = true;
	arrow.animate({transform:t,duration:180});
	statusLabel.text = "手を離すと更新";
      } else if (self.pulling && offset > -65.0 && offset < 0) {
	self.pulling = false;
	var t = Titanium.UI.create2DMatrix();
	arrow.animate({transform:t,duration:180});
	statusLabel.text = "Pull down to refresh...";
      }
    });

    self.tableView.addEventListener('scrollEnd',function(e){
      if (self.pulling && !self.reloading && e.contentOffset.y <= -65.0)  {
	self.reloading = true;
	self.pulling = false;
	arrow.hide();
	statusLabel.text = "更新しています...";
	self.tableView.setContentInsets({top:60},{animated:true});
	arrow.transform = Titanium.UI.create2DMatrix();
	self.beginReloading();
      }
    });

    self.tableView.headerPullView = tableHeader;
    self.tableView.setData(data);

    return self.win.add(self.tableView);
  },
  nextPage:function(){
    Titanium.API.info(this.pagenumber);
    this.pagenumber++;
    return this.pagenumber;
  },
  beginReloading:function(){
    var self = this;

    setTimeout(function(){
      Titanium.API.info(self.tableView);
      self.tableView.scrollToTop();
      self.reloading = false;
    },2000);
    var entry = {
      name:'hoge',
      age:35
    };
    var row = Ti.UI.createTableViewRow({
      data:entry,
      height:40
    });
    var title = Ti.UI.createLabel(styles["titleLabel"]);
    title.text = entry.name,
    row.add(title);

    return self.tableView.insertRowAfter(4,row,{});
  }
};