LoginSignup
19
19

More than 5 years have passed since last update.

【MVC実践編】画面スクロールでコンテンツ自動ロード「loadMore」の実装について

Posted at

コンテンツを画面に表示するとき、下にスクロールすれば自動的に次のコンテンツをロードしてくれる、という「loadMore」機能を考える
例えば、クラウド上に保存してある写真を画面に表示するような Webアプリを作るとする。

第1案

MVC を少しかじると、次のような実装が頭に浮かぶ。

# apis/image.coffee
angular.module("app").factory "getImageAPI", ->
  getImageAPI = (userId, offset, limit) ->
    # call API and return promise
  return getImageAPI

# models/image.coffee
angular.module("app").factory "ImageModel", ["getImageAPI", (getImageAPI) ->
  ImageModel = (userId) ->
    _pageNo = 0
    _pageSize = 10
    _images = []
    _isComplete
    self =
      loadMore: ->
        promise = getImageAPI userId, (_pageNo++) * _pageSize, _pageSize
          .then (response) ->
            data = response.data
            Array.prototype.push.apply _images, data.images
            _isComplete = data.isComplete
            return response
        return promise
      getUserId: -> userId
      getImages: -> _images
      isComplete: -> _isComplete
    return self

  return ImageModel
]

# controllers/image.coffee
angular.module("app").controller "ImageController", ["$scope", "$stateParams", "ImageModel", ($scope, $stateParams, ImageModel) ->
  userId = queryParams.userId
  imageModel = ImageModel userId
  $scope.images = imageModel.getImages()
  $scope.isComplete = -> imageModel.isComplete()
  $scope.loadMore = ->
    imageModel.loadMore().then (data) ->
      $scope.$broadcast 'scroll.infiniteScrollComplete'
]
-# templates/image.haml
%ion-list
  %ion-item(ng-repeat="image in images")
    %img(ng-src="{{ image.src }}")
%ion-infinite-scroll(on-infinite="loadMore()" ng-if="!isComplete()")

angularJS と ionic を使っているが他のライブラリでもこんな構成になるはずだ。

  • M
    • getImageAPI
      • API 通信を行う
    • ImageModel
      • ユーザーの写真を管理するモデルを提供する
      • loadMore メソッドを呼ぶと、内部で API 通信を行い、画像を追加で読み込む
  • C
    • ImageController
      • モデルである ImageModel と、ビューである templates/image.haml をつなぎとめるコントローラ
      • $scope に渡した値が、view にバインドされる。
      • $stateParams で、queryString を受け取ることができる
  • V
    • templates/image.haml
      • ion-list
        • コンテンツをリスト表示する
      • ion-item
        • 1つのコンテンツを表す
      • ion-infinite-scroll
        • このタグが画面上に現れ、かつ ng-iftrue を返す限り、on-infinite を実行する
        • on-infinite には、loadMore の実行が指示されている

第1案の問題点

第1案は、実は完全な MVC 構成とは言えない。何がおかしいのだろう。
一言で言えば、「loadMore は view の事情であって、 Model が管理するべきではない」ということである。

  • クラウド上に保存されている画像をどう表示するかは、view が決める
  • view 側の実装は、例えば次のようなものが考えられる
    • スクロールすれば自動的につぎの10件がロードされる
    • ページャーを用意し、次へボタンを押せば次の10件がロードされる
    • 画像が1枚だけ表示され、3秒ごとに次の画像に切り替わる
  • loadMore は、その中の1つの選択肢に過ぎない。
  • つまり、loadMore 関数が ImageModel の中にあるのはおかしい。

ImageModel は、あくまで offset, limit を受け取り、その分のデータを表示するべきだろう。

第2案

そこで、次のような util 関数を用意して、この切り分けを実現する。

# apis/image.coffee
angular.module("app").factory "getImageAPI", ->
  getImageAPI = (userId, offset, limit) ->
    # call API and return promise
  return getImageAPI

# models/image.coffee
angular.module("app").factory "ImageModel", ["getImageAPI", (getImageAPI) ->
  ImageModel = (userId) ->
    self =
      getImages: (offset, limit) ->
        promise = getImageAPI userId, offset, limit
          .then (response) ->
            return {
              images: response.data.images
              isComplete: response.data.isComplete
            }
        return promise
      getUserId: -> userId
    return self

  return ImageModel
]

# views/loadMore.coffee
angular.module("app").factory "view.loadMoreTask", ->
  return (startPage, loader) ->
    self =
      pageNo: startPage
      items: []
      isComplete: false
      loadMore: ->
        loader(self.pageNo++).then (response) ->
          Array.prototype.push.apply(self.items, response.items)
          self.isComplete = response.isComplete
    return self

# controllers/image.coffee
angular.module("app").controller "ImageController", ["$scope", "$stateParams", "ImageModel", "view.loadMoreTask", ($scope, $stateParams, ImageModel, loadMoreTask) ->
  userId = queryParams.userId
  imageModel = ImageModel userId
  pageSize = 10
  $scope.loadMoreTask = loadMoreTask 0, (pageNo) ->
    imageModel.getImages(pageNo * pageSize, pageSize).then (data) ->
      $scope.$broadcast 'scroll.infiniteScrollComplete'
      return {
        items: data.images
        isComplete: data.isComplete
      }
]
-# templates/image.haml
%ion-list
  %ion-item(ng-repeat="item in loadMoreTask.items")
    %img(ng-src="{{ item.src }}")
%ion-infinite-scroll(on-infinite="loadMoreTask.loadMore()" ng-if="!loadMoreTask.isComplete")
  • M は、V がどのように表示しているかは知らない
    • offset, limit を受け取って 画像を返すだけである
  • V は、M から受け取ったデータを表示するためのプレゼンテーションロジックを提供する。
    • このロジックは、データの中身が画像なのか、ツイートなのかなどは知らない。
  • C は、M と V とを結びつけ、M から受け取ったデータを V が提供するプレゼンテーションロジックに渡す。
19
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
19