オンラむンナヌザヌの無料リアルタむムリストParse.com + Pubnub

緎習甚の身近なタスクを䜜成したら、ログむン/ログむンボタンが1぀ずオンラむンナヌザヌリストがあるアプリケヌションを䜜成したす。 この堎合、ナヌザヌは30秒間だけ「ラむブ」する必芁がありたす。 垞に起こるように、私が最初にタスクを怜蚎したずき、私は思ったは、䜕をすべきか 私たちはナヌザヌにクラりドストレヌゞずサヌバヌを䜿甚したすが、状況は小さいのですが...



猫の䞋で、Parse.comでバック゚ンドを開発する際に盎面した問題、Pubnubず組み合わせお䜿甚​​する必芁がある理由、Android甚に開発する際にすべおを接続する方法に぀いお説明したす。



最埌に出おきたもの



デモンストレヌション





厳密に蚀えば、Parse.comずPubnubはすでにHabréの組み合わせの䞻題を扱っおいたす。 ただし、その蚘事ずは異なり、ここではParse.comクラりドコヌドに぀いお詳しく説明したす。タヌゲットアプリケヌションはIOSではなくAndroid甚に開発しおいたす。



Parse.com



Parse.comは広範なクラりド機胜を提䟛したす。ここでは、矎しいグラフィカルラッパヌ、サヌバヌコヌド、分析、さらにはプッシュ通知でデヌタベヌスを芋぀けるこずができたす。 そしお、1秒あたり30リク゚スト、20 GBのストレヌゞ䜿甚などのしきい倀を超えるたで、すべお無料です。 。 私はこれらの芁件に完党に満足したした無料ですので、この特定のサヌビスに遞択肢がありたした。



問題


ガむドを慎重に䜜成した結果、オンラむンナヌザヌのリアルタむムリストずその寿呜に関連するいく぀かの問題が浮䞊したした 。

  1. カスタムオブゞェクト甚のタむマヌタむプのフィヌルドはありたせん
  2. サヌビスは、 長い匕き寄せの可胜性を提䟛したせんたたは、私はこれを芋぀けたせんでした
  3. 暙準のUser / Sessionクラスはこのタスクには適しおいたせん


解決策


これらが問題である理由を説明させおください。

ナヌザヌが「死亡」したずきに通知を受信する理想的には自動的に削陀するために、ExpireAtフィヌルドずしおTimerタむプたたはそのようなものを䜿甚するこずが蚈画されおいたした。



このタむプが存圚しない堎合は、通垞の日付タむプを䜿甚し、ナヌザヌを「殺す」必芁があるずきに自分で監芖する必芁がありたす。



ロングプルを䜿甚しお着信/発信ナヌザヌを远跡するこずが蚈画されおいたしたが、このサヌビスはそのような機䌚をすぐに提䟛したせん。



むンストヌルを䜿甚するこずが決定されたした。 芁するに、これらは誰サヌバヌ-クラむアント、クラむアント-クラむアントなどの間でデヌタを転送するためのグロヌバルチャネルです。 したがっお、ログむン/ raloginの埌、サヌバヌは、そのようなナヌザヌが出入りしたずいうメッセヌゞをチャネルに送信する必芁がありたす。 これにより、ナヌザヌのリアルタむムリストが提䟛されたす。 ただし、これは完党に真実ではありたせんが、それに぀いおは埌で詳しく説明したす。



Parse.com SDKにはログむン/ログむンメ゜ッドが組み蟌たれおいるため、開発でそれらを䜿甚するず非垞に䟿利です。 しかし、これは䞍可胜であるこずが刀明したした。



䞊蚘のように、ログむン/ログアりトするずき、ナヌザヌが「生きおいる」たたはログアりトしおいる堎合は「死んでいる」かどうかに関するメッセヌゞをチャネルに送信する必芁がありたす。 このサヌビスは、AfterAfter、beforeDeleteなどのトリガヌを䜜成する機胜を提䟛したす。 問題は、セッションにそのようなむベントがないこずです。 これは、ログを蚘録するたびに、セッションでナヌザヌを文字通り削陀する必芁があるこずを意味したす。これにより、SDKに組み蟌たれたメ゜ッドの利点がすべお倱われたす。



したがっお、アラヌトがグロヌバルチャネルに送信されるafterDeleteおよびafterSaveトリガヌでハングするこずにより、カスタムクラスIMH_Sessionを䜿甚するこずが決定されたした。



ニュアンス


そしお、Android Studioで勝利を祝っお座る時間になりたすが、...むンストヌルはデフォルトのプッシュ通知に基づいおいたす。 私のように戊車に乗っおいる人のために説明したす。 プッシュ通知は䜕も保蚌したせん。 圌らはFireForgetの原則に基づいお動䜜したす。぀たり、プッシュ通知を送信するこずにより、受信者に到達したずいう確実性はありたせん。 さらに、それがい぀起こったかを蚀うこずさえ䞍可胜です



そのため、リアルタむムで話す必芁はありたせん。



たた、チャネルのフラッディングにも問題がありたす。 むンストヌルは同じであるため、どのクラむアントも䜕でも送信でき、残りはこのガベヌゞからサヌバヌメッセヌゞのみを遞択する必芁がありたす。 そしお、それは圌らがサヌバヌから来たずいう事実ではありたせん。 怜蚌の問題がありたす。 少なくずも、サヌバヌぞの各メッセヌゞには、ナヌザヌに関する情報の確認芁求を送信する必芁がありたす。これにより、混乱が生じ、1秒あたり30の非垞に無料の芁求がすぐに終了したす。



パブナブ



すべおの嘆かわしい状況にもかかわらず、解決策が芋぀かりたした。 これはPubnubでした 。 䞀般に、このサヌビスはすぐに䜿甚できるオンラむンチャット甚のSDKを提䟛したすが、残念ながら有料であり、アドオンず呌ばれたす。



サヌビス自䜓は、リアルタむムアプリケヌションに必芁なすべおを提䟛したす。 しかし、私たちが関心を持っおいるのは、リアルタむムチャンネルを攟送するこずです。 これらは無料で䜿いやすく、そしおおそらく最も重芁なこずには、アクセス制埡がありたす 怜蚌に煩わされないために必芁なもの。



差別化は、publish_keyずsubscribe_keyの2぀の別個のキヌによるものです。 ご想像のずおり、最初のキヌはサヌバヌに送られ、2番目のキヌはクラむアントアプリケヌションに送られたす。 キヌを最初に秘密にしおおくず、誰もチャネルをスパムせず、その䞭に瀺されたメッセヌゞを信頌できたす。 パヌフェクト



ps私はParse.comを曞いおいたすが、Pubnub `.com`なしは、私がそれに慣れおいるからです。 これが誰も傷぀けないこずを願っおいたす。



バック゚ンド-Parse.com



サヌバヌAPIずその実装の敎理を開始する必芁がありたした。 パむプラむンのアむデアは次のずおりであるこずを思い出させおください。

  1. カスタムAPI経由ログむン
  2. Parse.comクラりドコヌド
  3. デヌタベヌスでのナヌザヌ䜜成
  4. afterSaveトリガヌは、Pubnubチャネルにナヌザヌログむンに぀いお通知したす
  5. ログむンに応答しお珟圚のナヌザヌを返す


このために、次のAPIが䜜成されたした。



埌者に぀いお説明したす。 クラむアント時間ずサヌバヌ時間を同期するために䜿甚する予定でした。 API自䜓を実装したしたが、クラむアントでそれを䜿甚するこずはできたせんでした。 ただし、API自䜓はそのたたにするこずにしたした。



コヌドの匂いをおaびしたす。 決しおjs開発者。 その組織ぞの芁望を怜蚎したす。

サヌバヌコヌド
/*global Parse:false, $:false, jQuery:false */ // Importas var _ = require('underscore'); // jshint ignore:line var moment = require('moment'); // jshint ignore:line // Constants var sessionObjName = "IMH_Session"; var sessionLifetimeSec = 13; var channelName = "events"; var publishKey = "pub-c-6271f363-519a-432d-9059-e65a7203ce0e", subscribeKey = "sub-c-a3d06db8-410b-11e5-8bf2-0619f8945a4f", httpRequestUrl = 'http://pubsub.pubnub.com/publish/' + publishKey + '/' + subscribeKey + '/0/' + channelName + '/0/'; // Utils function Log(obj, tag) { "use strict"; var loggingString = "Cloud_code: "; if (tag != null) { // jshint ignore:line loggingString += "[" + tag + "] "; } loggingString += JSON.stringify(obj) + "\n"; console.log(loggingString); // jshint ignore:line } function GetNow() { "use strict"; return moment.utc(); } // Supporting var baseSession = {udid: "", loginedAt: GetNow(), aliveTo: GetNow()}; var errorHandler = function(error) { "use strict"; Log(error.message, "error"); }; function DeleteSession(obj) { obj.set("loginedAt", obj.get("aliveTo")); SendEvent(obj); obj.destroy(); } function DeleteDeadSessions() { "use strict"; var query = new Parse.Query(sessionObjName); // jshint ignore:line var promise = query.lessThanOrEqualTo("aliveTo", GetNow().toDate()) .each(function(obj) { Log(obj, "Delete dead session"); DeleteSession(obj); } ); return promise; } function NewSession(udid) { "use strict"; var session = _.clone(baseSession); session.udid = udid; session.loginedAt = GetNow(); session.aliveTo = GetNow().add({seconds: sessionLifetimeSec}); return session; } function GetSessionQuery() { "use strict"; var objConstructor = Parse.Object.extend(sessionObjName); // jshint ignore:line var query = new Parse.Query(objConstructor); //query.select("udid", "loginedAt", "aliveTo"); //not work for some reason return query; } function IsUserOnline(udid, onUserOnlineHanlder, onUserOfflineHanlder, onError) { "use strict"; var userAlive = false; var query = GetSessionQuery(); query.equalTo("udid", udid).greaterThanOrEqualTo("aliveTo", GetNow().toDate()); query.find({ success: function(result) { if (result.length == 0) { onUserOfflineHanlder(); } else { onUserOnlineHanlder(result); } }, error: onError }); } function NewParseSession(session) { "use strict"; var objConstructor = Parse.Object.extend(sessionObjName); // jshint ignore:line var obj = new objConstructor(); obj.set({ udid: session.udid, loginedAt: session.loginedAt.toDate(), aliveTo: session.aliveTo.toDate() } ); return obj; } function SendEvent(session) { "use strict"; Parse.Cloud.httpRequest({ // jshint ignore:line url: httpRequestUrl + JSON.stringify(session), success: function(httpResponse) {}, error: function(httpResponse) { Log('Request failed with response code ' + httpResponse.status); } }); } // API functions var API_GetNow = function(request, response) { "use strict"; var onUserOnline = function(result) { response.success( GetNow().toDate() ); }; var onUserOffline = function(error) { response.error(error); }; var onError = function(error) { response.error(error); }; IsUserOnline(request.params.udid, onUserOnline, onUserOffline, onError); }; var API_GetOnlineUsers = function(request, response) { "use strict"; var onUserOnline = function(result) { var query = GetSessionQuery() .addDescending("aliveTo"); query.find({ success: function(result) { response.success( JSON.stringify(result) ); }, error: errorHandler }); }; var onUserOffline = function(error) { response.error(error); }; var onError = function(error) { response.error(error); }; DeleteDeadSessions().always( function() { IsUserOnline(request.params.udid, onUserOnline, onUserOffline, onError); }); }; var API_Login = function(request, response) { "use strict"; var userUdid = request.params.udid; var session = NewSession(userUdid); var parseObject = NewParseSession(session); Parse.Cloud.run("Logout", {udid: userUdid}).always( function() { parseObject.save(null, { success: function(obj) { Log(obj, "Login:save"); response.success( JSON.stringify(parseObject) ); }, error: function(error) { errorHandler(error); response.error(error); } }); }); }; var API_Logout = function(request, response) { "use strict"; var userUdid = request.params.udid; var query = GetSessionQuery() .equalTo("udid", userUdid); query.each( function(obj) { Log(obj, "Logout:destroy"); DeleteSession(obj); }).done( function() {response.success();} ); }; // Bindings Parse.Cloud.afterSave(sessionObjName, function(request) { // jshint ignore:line "use strict"; SendEvent(request.object); }); // API definitions Parse.Cloud.define("GetNow", API_GetNow); // jshint ignore:line Parse.Cloud.define("GetOnlineUsers", API_GetOnlineUsers); // jshint ignore:line Parse.Cloud.define("Login", API_Login); // jshint ignore:line Parse.Cloud.define("Logout", API_Logout); // jshint ignore:line
      
      







ご芧のずおり、afterDeleteトリガヌなしで実行したした。 その理由は、afterDeleteでレヌスをしたからです。 䞀方、新しくリリヌスされたナヌザヌは削陀され、すぐにチャネルにアラヌトが送信されたす。 䞀方、圌はすぐに再床ログむンしようずしたす。

その結果、チャネルには「Xログむン枈み」、「Xログむン枈み」、「Xログアりト枈み」などが衚瀺されたす。 最埌の2぀のメッセヌゞは䞍適切です。 このため、ナヌザヌがただ「生きおいる」ようでログむンしおいるように芋えるが、オンラむンリストには衚瀺されないずいう状況がクラむアントにありたした。



より倚くのニュアンス


前述のように、Parse.comでは、いく぀かのTimerではなくDateを䜿甚しお、expireAtこの堎合はaliveToを敎理する必芁がありたす。 しかし、ここに質問がありたす-そしお、すべおのナヌザヌが「生きおいる」かすでに「死んでいる」かどうかをい぀チェックするのですか



1぀の解決策は、 ゞョブを䜿甚しお、非アクティブなナヌザヌを5〜10秒ごずに削陀するこずです。 しかし、厳密に蚀えば、これは非垞にリアルタむムではありたせん。 䜕らかの皮類のバックグラりンドゞョブに関係なく、ナヌザヌを即座に "死に"させたかったずころで、最倧実行時間に制限がありたす-15分。したがっお、垞に再䜜成する必芁がありたす。 したがっお、別のアプロヌチが実装されたした。



通垞のナヌザヌラむフは次のようになりたす。



ログむン-> GetOnlineUsers->ログアりト



たたは



ログむン-> GetOnlineUsers->アプリケヌションの最小化、぀たり、チャンネル内のメッセヌゞの欠萜-> GetOnlineUsers->ログアりト



誰かがGetOnlineUsersに尋ねた瞬間に「死んだ」ナヌザヌを削陀するこずが決定されたした。 これは、実際には、少なくずも誰かが「ラむブ」ナヌザヌのリストを芁求するたで、「デッド」ナヌザヌをデヌタベヌスに保存できるこずを意味したす。 この時点で、すべおのデッドナヌザヌが削陀されたすレむゞヌコンピュヌティングの最高の䌝統。



したがっお、ナヌザヌの「生掻」はクラむアント䞊でロヌカルに監芖する必芁がありたす。 ナヌザヌの死亡に関するチャンネル内の通知は、ナヌザヌが自分でログアりトした堎合にのみ送信されたす。 それ以倖の堎合、ナヌザヌは氞遠に生きおいるず芋なされたす。



Android



パブナブ


Pubnub SDK、たたはその無料の郚分は非垞に䜿いやすいです。 たず、Pubnubのラッパヌを䜜成しお、もしあれば、他のサヌビスを䜿甚できるようにしたした。



Pubnub Wrap-チャンネル
 public class PubnubChannel extends Channel { static private final String CHANNEL_NAME = "events"; static private final String SUBSCRIBE_KEY = "sub-c-a3d06db8-410b-11e5-8bf2-0619f8945a4f"; Pubnub pubnub = new Pubnub("", SUBSCRIBE_KEY); Callback pubnubCallback = new Callback() { @Override public void connectCallback(String channel, Object message) { if (listener != null) { listener.onConnect(channel, "Connected: " + message.toString()); } } @Override public void disconnectCallback(String channel, Object message) { if (listener != null) { listener.onDisconnect(channel, "Disconnected: " + message.toString()); } } @Override public void reconnectCallback(String channel, Object message) { if (listener != null) { listener.onReconnect(channel, "Reconnected: " + message.toString()); } } @Override public void successCallback(String channel, Object message, String timetoken) { if (listener != null) { listener.onMessageRecieve(channel, message.toString(), timetoken); } } @Override public void errorCallback(String channel, PubnubError error) { if (listener != null) { listener.onErrorOccur(channel, "Error occured: " + error.toString()); } } }; public PubnubChannel() { setName(CHANNEL_NAME); } @Override public void subscribe() throws ChannelException { try { pubnub.subscribe(CHANNEL_NAME, pubnubCallback); } catch (PubnubException e) { e.printStackTrace(); throw new ChannelException(ChannelException.CONNECT_ERROR, e); } } @Override public void unsubscribe() { pubnub.unsubscribeAll(); } }
      
      







次に、チャネル内の䞀郚のメッセヌゞではなく、特定のナヌザヌを远跡するために、オヌバヌラップはい、はいにラッパヌが䜜成されたした。



チャンネルラッパヌ-ServerChannel
 public class ServerChannel { Logger l = LoggerFactory.getLogger(ServerChannel.class); JsonParser jsonParser; Channel serverChannel; ServerChannel.EventListener listener; private final Channel.EventListener listenerAdapter = new Channel.EventListener() { @Override public void onConnect(String channel, String greeting) { } @Override public void onDisconnect(String channel, String reason) { if (listener != null) { listener.onDisconnect(reason); } } @Override public void onReconnect(String channel, String reason) { } @Override public void onMessageRecieve(String channel, String message, String timetoken) { if (listener != null) { ServerChannel.this.onMessageRecieve(message, timetoken); } } @Override public void onErrorOccur(String channel, String error) { l.warn(String.format("%s : [error] %s", channel, error)); if (listener != null) { ServerChannel.this.unsubscribe(); } } }; public ServerChannel(Channel serverChannel, JsonParser jsonParser) { this.serverChannel = serverChannel; this.jsonParser = jsonParser; } public final void setListener(@NonNull ServerChannel.EventListener listener) { this.listener = listener; } public final void clearListener() { listener = null; } public final void subscribe() throws ChannelException { try { serverChannel.setListener(listenerAdapter); serverChannel.subscribe(); } catch (ChannelException e) { e.printStackTrace(); serverChannel.clearListener(); throw e; } } public final void unsubscribe() { serverChannel.unsubscribe(); serverChannel.clearListener(); } public void onMessageRecieve(String userJson, String timetoken) { DyingUser dyingUser = jsonParser.fromJson(userJson, DyingUser.class); if (dyingUser != null) { if (dyingUser.isAlive()) { listener.onUserLogin(dyingUser); } else { listener.onUserLogout(dyingUser); } } } public interface EventListener { void onDisconnect(String reason); void onUserLogin(DyingUser dyingUser); void onUserLogout(DyingUser dyingUser); } }
      
      







Parse.com


繰り返したすが、䜕も耇雑ではありたせん。 すべおのロゞックはサヌバヌに保存されたす。 必芁なのは、APIを䜿甚しおjsonをオブゞェクトに解析するこずだけです。



Authapi
 public class AuthApi extends Api { static final String API_Login = "Login", API_Logout = "Logout"; @Inject public AuthApi(JsonParser parser) { super(parser); } public DyingUser login(@NonNull final String udid) throws ApiException { DyingUser dyingUser; try { String jsonObject = ParseCloud.callFunction(API_Login, constructRequestForUser(udid)); dyingUser = parser.fromJson(jsonObject, DyingUser.class); } catch (ParseException e) { e.printStackTrace(); throw new ApiException(ApiException.LOGIN_ERROR, e); } return dyingUser; } public void logout(@NonNull final DyingUser dyingUser) { try { ParseCloud.callFunction(API_Logout, constructRequestForUser(dyingUser.getUdid())); } catch (ParseException e) { e.printStackTrace(); } } }
      
      







UserApi
 public class UserApi extends Api { static final String API_GetOnlineUsers = "GetOnlineUsers"; @Inject public UserApi(JsonParser parser) { super(parser); } public final ArrayList<DyingUser> getOnlineUsers(@NonNull final DyingUser dyingUser) throws ApiException { ArrayList<DyingUser> users; try { String jsonUsers = ParseCloud.callFunction(API_GetOnlineUsers, constructRequestForUser(dyingUser.getUdid())); users = parser.fromJson(jsonUsers, new TypeToken<List<DyingUser>>(){}.getType()); } catch (ParseException e) { e.printStackTrace(); throw new ApiException(ApiException.GET_USERS_ERROR, e); } return users; } }
      
      







さお、基本クラス



API
 abstract class Api { final JsonParser parser; Api(JsonParser parser) { this.parser = parser; } protected Map<String, ?> constructRequestForUser(@NonNull final String udid) { Map<String, String> result = new HashMap<>(); result.put("udid", udid); return result; } }
      
      







䞊蚘のクラスずそのメ゜ッドを䜿甚しお、ログむンにアクセスし、ログむンしお、ナヌザヌのオンラむンリストを取埗したす。



リアルタむム


UIの曎新


ナヌザヌは「死ぬ」ため、非垞に迅速であるため、残りのラむフタむムを衚瀺するこずが決定されたした。 ラむフタむムは秒単䜍で枬定され、タスクの目暙はリアルタむムを確保するこずなので、UIは少なくずも1秒に1回曎新する必芁がありたす。 これを行うために、TimeTickerクラスが䜜成され、そのオブゞェクトはActivityに保存されたす。 onAttach䞭のアクティビティフラグメントは、ActivityからTimeTickerオブゞェクトを受け取りこれにTimeTicker.Ownerむンタヌフェむスが䜿甚されたす、そのむベントをサブスクラむブしたす。



タむムティッカヌ
 public class TimeTicker extends Listenable<TimeTicker.EventListener> { private static final long TICKING_PERIOD_MS_DEFAULT = 1000; private static final boolean DO_INSTANT_TICK_ON_START_DEFAULT = true; long tickingPeriodMs; boolean doInstantTickOnStart; final Handler uiHandler = new Handler(Looper.getMainLooper()); final Timer tickingTimer = new Timer(); TimerTask tickingTask; public TimeTicker() { this(DO_INSTANT_TICK_ON_START_DEFAULT); } public TimeTicker(boolean doInstantTickOnStart) { this.doInstantTickOnStart = doInstantTickOnStart; setTickingPeriodMs(TICKING_PERIOD_MS_DEFAULT); } public void setTickingPeriodMs(final long tickingPeriodMs) { this.tickingPeriodMs = tickingPeriodMs; } public synchronized void start() { if (tickingTask != null) { stop(); } tickingTask = new TimerTask() { @Override public void run() { uiHandler.post(new Runnable() { @Override public void run() { forEachListener(new ListenerExecutor<TimeTicker.EventListener>() { @Override public void run() { getListener().onSecondTick(); } }); } }); } }; long delay = (doInstantTickOnStart) ? 0 : tickingPeriodMs; tickingTimer.scheduleAtFixedRate(tickingTask, delay, tickingPeriodMs); } public synchronized void stop() { if (tickingTask != null) { tickingTask.cancel(); } tickingTask = null; tickingTimer.purge(); } public interface EventListener extends Listenable.EventListener { void onSecondTick(); } public interface Owner { TimeTicker getTimeTicker(); } }
      
      







これにより、UIが1秒間に1回曎新されるようになりたす。぀たり、すべおがナヌザヌの死にかけおいるように芋えたす。



「死にかけおいる」ナヌザヌのリスト


この問題は、このタスクに関連する最も興味深いもののように思えたした。「死にかけおいる」ナヌザヌのリストがありたす。 時間はれロに近づき、これが発生した堎合、ナヌザヌをリストから削陀する必芁がありたす。



最も簡単な実装は、各ナヌザヌにタむマヌを付加し、「死」に達したずきに削陀するこずです。 ただし、これは特に興味深い解決策ではありたせん。 倒錯したしょう ここに、単䞀のタむマヌず䞀時停止/再開の可胜性を備えたこのような実装がありたすたずえば、アプリケヌションが最小化されおいる堎合に䟿利です。



初めお曞いたので、このコヌドをリファクタリングしたこずがないので、あたり良くないかもしれたせん。



TemporarySet
 public class TemporarySet<TItem> extends Listenable<TemporarySet.EventListener> implements Resumable { protected final SortedSet<TemporaryElement<TItem>> sortedElementsSet = new TreeSet<>(); protected final List<TItem> list = new ArrayList<>(); protected final Timer timer = new Timer(); protected TimerTask timerTask = null; protected TemporaryElement<TItem> nextElementToDie = null; boolean isResumed = false; public TemporarySet() { notifier = new TemporarySet.EventListener() { @Override public void onCleared() { for (TemporarySet.EventListener listener : getListenersSet()) { listener.onCleared(); } } @Override public void onAdded(Object item) { for (TemporarySet.EventListener listener : getListenersSet()) { listener.onAdded(item); } } @Override public void onRemoved(Object item) { for (TemporarySet.EventListener listener : getListenersSet()) { listener.onRemoved(item); } } }; } public boolean add(TItem object, DateTime deathTime) { TemporaryElement<TItem> element = new TemporaryElement<>(object, deathTime); return _add(element); } public boolean remove(TItem object) { TemporaryElement<TItem> element = new TemporaryElement<>(object); return _remove(element); } public void clear() { _clear(); } public final List<TItem> asReadonlyList() { return Collections.unmodifiableList(list); } private synchronized void _clear() { cancelNextDeath(); list.clear(); sortedElementsSet.clear(); notifier.onCleared(); } private synchronized boolean _add(TemporaryElement<TItem> insertingElement) { boolean wasInserted = _insertElementUnique(insertingElement); if (wasInserted) { if (nextElementToDie != null && nextElementToDie.deathTime.isAfter(insertingElement.deathTime)) { cancelNextDeath(); } if (nextElementToDie == null) { openNextDeath(); } notifier.onAdded(insertingElement.object); } return wasInserted; } private synchronized boolean _remove(TemporaryElement<TItem> deletingElement) { boolean wasDeleted = _deleteElementByObject(deletingElement); if (wasDeleted) { if (nextElementToDie.equals(deletingElement)) { cancelNextDeath(); openNextDeath(); } notifier.onRemoved(deletingElement.object); } return wasDeleted; } private synchronized void openNextDeath() { cancelNextDeath(); if (sortedElementsSet.size() != 0) { nextElementToDie = sortedElementsSet.first(); timerTask = new TimerTask() { @Override public void run() { _remove(nextElementToDie); } }; DateTime now = new DateTime(); Duration duration = TimeUtils.GetNonNegativeDuration(now, nextElementToDie.deathTime); timer.schedule(timerTask, duration.getMillis()); } } private synchronized void cancelNextDeath() { if (timerTask != null) { timerTask.cancel(); } timer.purge(); nextElementToDie = null; timerTask = null; } private synchronized Iterator<TemporaryElement<TItem>> findElement(TemporaryElement<TItem> searchingElement) { Iterator<TemporaryElement<TItem>> resultIterator = null; for (Iterator<TemporaryElement<TItem>> iterator = sortedElementsSet.iterator(); iterator.hasNext() && resultIterator == null;) { if (iterator.next().equals(searchingElement)) { resultIterator = iterator; } } return resultIterator; } private synchronized boolean _insertElementUnique(TemporaryElement<TItem> element) { boolean wasInserted = false; Iterator<TemporaryElement<TItem>> iterator = findElement(element); if (iterator == null) { wasInserted = true; sortedElementsSet.add(element); list.add(element.object); } return wasInserted; } private synchronized boolean _deleteElementByObject(TemporaryElement<TItem> element) { boolean wasDeleted = false; Iterator<TemporaryElement<TItem>> iterator = findElement(element); if (iterator != null) { wasDeleted = true; iterator.remove(); list.remove(element.object); } return wasDeleted; } @Override public void resume() { isResumed = true; openNextDeath(); } @Override public void pause() { cancelNextDeath(); isResumed = false; } @Override public boolean isResumed() { return isResumed; } public interface EventListener extends Listenable.EventListener { void onCleared(); void onAdded(Object item); void onRemoved(Object item); } }
      
      







ここでは䜿甚しないasReadonlyListメ゜ッドがあるこずに泚意しおください。 以前は、ListFragmentのアダプタヌ匕数ずしお䜿甚されおいたため、EventListenerをたったく䜿甚できたせんでした。 しかし、埌で私はこのベンチャヌから離れるこずに決めたしたが、コヌドを残すこずにしたした将来のために、どうすれば䟡倀がないかを芋るため。



このリスト内の最倧のバッカニアは、メ゜ッドfindElement、_insertElementUniqueおよび_deleteElementByObjectで䜜成されたす。 その理由は、SortedSetは日付順に゜ヌトされたオブゞェクトを栌玍するため、怜玢も日付ごずに行われるためです。 ただし、ナヌザヌが「死亡」するず、サヌバヌはloginedAt == deathAtずいうメッセヌゞを送信したす。これにより、SortedSetずTemporarySet党䜓が狂気になりたす。



Javaには通垞のペア<A、B>  upd Bringoffが正しく指摘したように、 ただありたす がないため、ラッパヌが実装されたした



TemporaryElement
 class TemporaryElement<T> implements Comparable { protected final T object; protected final DateTime deathTime; public TemporaryElement(@NonNull T object, @NonNull DateTime deathTime) { this.deathTime = deathTime; this.object = object; } public TemporaryElement(@NonNull T object) { this(object, new DateTime(0)); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TemporaryElement<?> that = (TemporaryElement<?>) o; return object.equals(that.object); } @Override public int hashCode() { return object.hashCode(); } @Override public int compareTo(@NonNull Object another) { TemporaryElement a = this, b = (TemporaryElement) another; int datesComparisionResult = a.deathTime.compareTo(b.deathTime); int objectsComparisionResult = a.hashCode() - b.hashCode(); return (datesComparisionResult != 0) ? datesComparisionResult : objectsComparisionResult; } }
      
      







その結果、実装されたTemporarySetでは、存続期間のあるナヌザヌを远加たたは削陀できたす。その埌、TemporarySet.EventListenerむンタヌフェむスを実装しお埅機するだけです。



おわりに



タスクは圓初の蚈画よりも困難でした。 Parse.comガむドの解析に倚くの時間を費やしたした。 ニュアンスの1぀の䟋を次に瀺したす。



afterSave
 Parse.Cloud.afterSave("Foo", function(request) {}); // custom Foo object Parse.Cloud.afterSave("User", function(request) {}); // custom(!) User object Parse.Cloud.afterSave(Parse.User, function(request) {}); // Parse.com User object Parse.Cloud.afterSave(Parse.Session, function(request) {}); // error! can't bind to Parse.Session
      
      







グラデヌションアニメヌションにはさらに倚くの時間が費やされたした。 より正確には、アニメヌションではなく、タヌンキヌ゜リュヌションを芋぀けるこずではありたせん。 残念ながら、自分に適した方法が芋぀からなかったため、解決策を曞きたした。 私は英語でスタックオヌバヌフロヌを詳现に描きたした 。



すべおの私のコヌドはここで芋るこずができたす 。



公平を期しお、GetUsersChangesAfterDateのようなものをAPIに远加するず、指定した日付の埌にナヌザヌリストの倉曎を取埗できるようになりたす぀たり、アプリケヌションを最小化->展開-> GetUsersChangesAfterDate。



最埌に、読者にいく぀か質問をしたいず思いたす。

  1. これは簡単ですが、無料でもできたすか
  2. N秒ごずにUIを曎新する簡単な方法はありたすか
  3. ナヌザヌの生涯「00」をどうしたすか ナヌザヌが「01」の埌に「死ぬ」ように、ラむフタむムに人為的に1秒を远加する必芁がありたすか それずも別の方法で決定されたすか たたは、「00」のたたにしたす-これは正垞ですか



All Articles