การเปิดใช้ความสามารถออฟไลน์บน Android

แอปพลิเคชัน Firebase จะทํางานได้แม้ว่าแอปจะตัดการเชื่อมต่อเครือข่ายชั่วคราว นอกจากนี้ Firebase ยังมีเครื่องมือสำหรับเก็บข้อมูลไว้ในเครื่อง จัดการการแสดงข้อมูล และจัดการเวลาในการตอบสนอง

ข้อมูลคงที่ของดิสก์

แอป Firebase จะจัดการการหยุดชะงักของเครือข่ายชั่วคราวโดยอัตโนมัติ ข้อมูลแคชจะพร้อมใช้งานขณะออฟไลน์ และ Firebase จะส่งการเขียนอีกครั้งเมื่อการเชื่อมต่อเครือข่ายกลับมาทำงานอีกครั้ง

เมื่อเปิดใช้การคงข้อมูลไว้ในดิสก์ แอปจะเขียนข้อมูลลงในอุปกรณ์แบบในเครื่องเพื่อให้แอปคงสถานะไว้ได้ขณะออฟไลน์ แม้ว่าผู้ใช้หรือระบบปฏิบัติการจะรีสตาร์ทแอปก็ตาม

คุณเปิดใช้การคงข้อมูลในดิสก์ได้โดยใช้โค้ดเพียง 1 บรรทัด

Kotlin+KTX

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

ลักษณะการคงอยู่

เมื่อเปิดใช้การถาวร ข้อมูลที่ไคลเอ็นต์ Firebase Realtime Database จะซิงค์ขณะที่ออนไลน์ยังคงอยู่ในดิสก์และใช้งานแบบออฟไลน์ได้ แม้ว่าผู้ใช้หรือระบบปฏิบัติการจะรีสตาร์ทแอป ซึ่งหมายความว่าแอปจะทำงานเหมือนกับที่ออนไลน์อยู่โดยใช้ข้อมูลในเครื่องที่จัดเก็บไว้ในแคช ฟังก์ชันการเรียกกลับของ Listener จะยังคงทํางานสําหรับการอัปเดตในเครื่อง

ไคลเอ็นต์ Firebase Realtime Database จะจัดคิวการดำเนินการเขียนทั้งหมดโดยอัตโนมัติขณะที่แอปออฟไลน์ เมื่อเปิดใช้การคงข้อมูลไว้ ระบบจะคงคิวนี้ไว้ในดิสก์ด้วยเพื่อให้การเขียนทั้งหมดพร้อมใช้งานเมื่อผู้ใช้หรือระบบปฏิบัติการรีสตาร์ทแอป เมื่อแอปเชื่อมต่ออีกครั้ง ระบบจะส่งการดำเนินการทั้งหมดไปยังเซิร์ฟเวอร์ Firebase Realtime Database

หากแอปใช้ การตรวจสอบสิทธิ์ของ Firebase ไคลเอ็นต์ Firebase Realtime Database จะยังคงเก็บโทเค็นการตรวจสอบสิทธิ์ ของผู้ใช้เมื่อมีการรีสตาร์ทแอป หากโทเค็นการตรวจสอบสิทธิ์หมดอายุขณะที่แอปออฟไลน์อยู่ ไคลเอ็นต์จะหยุดการดำเนินการเขียนชั่วคราวจนกว่าแอปจะตรวจสอบสิทธิ์ผู้ใช้อีกครั้ง ไม่เช่นนั้นการดำเนินการเขียนอาจไม่สำเร็จเนื่องจากกฎด้านความปลอดภัย

การรักษาข้อมูลให้ใหม่อยู่เสมอ

Firebase Realtime Database จะซิงค์ข้อมูลและจัดเก็บสำเนาข้อมูลในเครื่องสำหรับผู้ฟังที่ใช้งานอยู่ นอกจากนี้ คุณยังซิงค์สถานที่ตั้งที่เฉพาะเจาะจงได้

Kotlin+KTX

val scoresRef = Firebase.database.getReference("scores")
scoresRef.keepSynced(true)

Java

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);

ไคลเอ็นต์ Firebase Realtime Database จะดาวน์โหลดข้อมูลในตำแหน่งเหล่านี้โดยอัตโนมัติและซิงค์ข้อมูลอยู่เสมอ แม้ว่าข้อมูลอ้างอิงจะไม่มีผู้ฟังที่ใช้งานอยู่ก็ตาม คุณปิดการซิงค์อีกครั้งได้โดยใช้บรรทัดโค้ดต่อไปนี้

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

โดยค่าเริ่มต้น ระบบจะแคชข้อมูลที่ซิงค์ไว้ก่อนหน้านี้ 10 MB ซึ่งน่าจะเพียงพอสำหรับแอปพลิเคชันส่วนใหญ่ หากแคชมีขนาดใหญ่เกินขนาดที่กำหนดไว้ Firebase Realtime Database จะล้างข้อมูลที่ไม่ได้ใช้ล่าสุด ระบบจะไม่ล้างข้อมูลที่มีการซิงค์ออกจากแคช

การค้นหาข้อมูลแบบออฟไลน์

Firebase Realtime Database จะจัดเก็บข้อมูลที่แสดงผลจากการค้นหาเพื่อใช้เมื่อออฟไลน์ สำหรับการค้นหาที่สร้างขึ้นในขณะออฟไลน์ Firebase Realtime Database จะยังคงทำงานต่อไปสำหรับข้อมูลที่โหลดก่อนหน้านี้ หากยังไม่ได้โหลดข้อมูลที่ขอ Firebase Realtime Database จะโหลดข้อมูลจากแคชในเครื่อง เมื่อเชื่อมต่อเครือข่ายได้อีกครั้ง ข้อมูลจะโหลดและจะแสดงการค้นหา

ตัวอย่างเช่น โค้ดนี้จะค้นหา 4 รายการสุดท้ายในคะแนน Firebase Realtime Database

Kotlin+KTX

val scoresRef = Firebase.database.getReference("scores")
scoresRef.orderByValue().limitToLast(4).addChildEventListener(object : ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) {
        Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}")
    }

    // ...
})

Java

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.orderByValue().limitToLast(4).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) {
        Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }

    // ...
});

สมมติว่าผู้ใช้ขาดการเชื่อมต่อ ออฟไลน์ และรีสตาร์ทแอป ขณะที่ยังออฟไลน์ แอปจะค้นหาสำหรับ 2 รายการสุดท้ายจาก ตำแหน่งเดียวกัน การค้นหานี้จะแสดงรายการ 2 รายการสุดท้ายได้สําเร็จเนื่องจากแอปโหลดรายการทั้งหมด 4 รายการในการค้นหาด้านบนแล้ว

Kotlin+KTX

scoresRef.orderByValue().limitToLast(2).addChildEventListener(object : ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) {
        Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}")
    }

    // ...
})

Java

scoresRef.orderByValue().limitToLast(2).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) {
        Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }

    // ...
});

ในตัวอย่างก่อนหน้านี้ ไคลเอ็นต์ Firebase Realtime Database จะเพิ่มเหตุการณ์ "เพิ่มรายการย่อย" สําหรับไดโนเสาร์ 2 ตัวที่มีคะแนนสูงสุด โดยใช้แคชที่เก็บไว้ แต่จะไม่สร้างเหตุการณ์ "value" เนื่องจากแอปไม่เคยเรียกใช้การค้นหานั้นขณะออนไลน์

หากแอปขอรายการ 6 รายการล่าสุดขณะออฟไลน์ แอปจะได้รับเหตุการณ์ "เพิ่มรายการย่อย" สำหรับรายการที่แคชไว้ 4 รายการทันที เมื่ออุปกรณ์กลับมาออนไลน์ ไคลเอ็นต์ Firebase Realtime Database จะซิงค์กับเซิร์ฟเวอร์และรับเหตุการณ์ "เพิ่มรายการย่อย" และ "ค่า" 2 รายการสุดท้ายสําหรับแอป

การจัดการธุรกรรมแบบออฟไลน์

ระบบจะจัดคิวธุรกรรมทั้งหมดที่ดำเนินการขณะที่แอปออฟไลน์ เมื่อแอปเชื่อมต่อเครือข่ายได้อีกครั้งแล้ว ระบบจะส่งธุรกรรมไปยังเซิร์ฟเวอร์ Realtime Database

การจัดการสถานะอยู่

ในแบบเรียลไทม์ บ่อยครั้งที่การตรวจหาเมื่อไคลเอ็นต์เชื่อมต่อและยกเลิกการเชื่อมต่อจึงมีประโยชน์ เช่น คุณอาจต้องการทําเครื่องหมายผู้ใช้เป็น "ออฟไลน์" เมื่อไคลเอ็นต์ของผู้ใช้ตัดการเชื่อมต่อ

ไคลเอ็นต์ฐานข้อมูล Firebase มีองค์ประกอบพื้นฐานง่ายๆ ที่คุณสามารถใช้เขียนลงในฐานข้อมูลได้เมื่อไคลเอ็นต์ตัดการเชื่อมต่อจากเซิร์ฟเวอร์ฐานข้อมูล Firebase การอัปเดตเหล่านี้จะเกิดขึ้นไม่ว่าไคลเอ็นต์จะตัดการเชื่อมต่ออย่างเรียบร้อยหรือไม่ คุณจึงใช้การอัปเดตเหล่านี้เพื่อล้างข้อมูลได้แม้ว่าการเชื่อมต่อจะขาดหรือไคลเอ็นต์จะขัดข้องก็ตาม การดำเนินการเขียนทั้งหมด ซึ่งรวมถึงการตั้งค่า การอัปเดต และการนำออก จะดำเนินการได้เมื่อยกเลิกการเชื่อมต่อ

ต่อไปนี้เป็นตัวอย่างง่ายๆ ของการเขียนข้อมูลเมื่อยกเลิกการเชื่อมต่อโดยใช้ onDisconnect แบบเดิม

Kotlin+KTX

val presenceRef = Firebase.database.getReference("disconnectmessage")
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!")

Java

DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!");

วิธีการทำงานของการยกเลิกการเชื่อมต่อ

เมื่อคุณสร้างการดำเนินการ onDisconnect() แล้ว การดำเนินการนั้นจะอยู่ในเซิร์ฟเวอร์ Firebase Realtime Database เซิร์ฟเวอร์จะตรวจสอบความปลอดภัยเพื่อให้แน่ใจว่าผู้ใช้จะดำเนินการตามกิจกรรมการเขียนที่ขอได้ และจะแจ้งแอปของคุณหากแอปไม่ถูกต้อง จากนั้นเซิร์ฟเวอร์จะตรวจสอบการเชื่อมต่อ หากเมื่อใดก็ตามที่การเชื่อมต่อหมดเวลา หรือไคลเอ็นต์ Realtime Database ปิดอยู่ในขณะนี้ เซิร์ฟเวอร์จะตรวจสอบความปลอดภัยเป็นครั้งที่ 2 (เพื่อให้แน่ใจว่าการดำเนินการยังคงใช้งานได้) จากนั้นเรียกใช้เหตุการณ์

แอปของคุณสามารถใช้การเรียกกลับในการดำเนินการเขียนเพื่อตรวจสอบว่าได้แนบ onDisconnect อย่างถูกต้องแล้ว

Kotlin+KTX

presenceRef.onDisconnect().removeValue { error, reference ->
    error?.let {
        Log.d(TAG, "could not establish onDisconnect event: ${error.message}")
    }
}

Java

presenceRef.onDisconnect().removeValue(new DatabaseReference.CompletionListener() {
    @Override
    public void onComplete(DatabaseError error, @NonNull DatabaseReference reference) {
        if (error != null) {
            Log.d(TAG, "could not establish onDisconnect event:" + error.getMessage());
        }
    }
});

นอกจากนี้ คุณยังยกเลิกกิจกรรม onDisconnect ได้โดยเรียกใช้ .cancel() ดังนี้

Kotlin+KTX

val onDisconnectRef = presenceRef.onDisconnect()
onDisconnectRef.setValue("I disconnected")
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel()

Java

OnDisconnect onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.setValue("I disconnected");
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel();

สถานะการตรวจสอบการเชื่อมต่อ

สําหรับฟีเจอร์ที่เกี่ยวข้องกับการแสดงข้อมูลความพร้อมหลายรายการ การที่แอปทราบว่าออนไลน์หรือออฟไลน์อยู่จะมีประโยชน์ Firebase Realtime Database ระบุตำแหน่งพิเศษที่ /.info/connected ซึ่งจะอัปเดตทุกครั้งที่สถานะการเชื่อมต่อของไคลเอ็นต์ Firebase Realtime Database เปลี่ยนแปลง ตัวอย่างมีดังนี้

Kotlin+KTX

val connectedRef = Firebase.database.getReference(".info/connected")
connectedRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val connected = snapshot.getValue(Boolean::class.java) ?: false
        if (connected) {
            Log.d(TAG, "connected")
        } else {
            Log.d(TAG, "not connected")
        }
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled")
    }
})

Java

DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        boolean connected = snapshot.getValue(Boolean.class);
        if (connected) {
            Log.d(TAG, "connected");
        } else {
            Log.d(TAG, "not connected");
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled");
    }
});

/.info/connected เป็นค่าบูลีนที่ไม่ซิงค์ระหว่างไคลเอ็นต์ Realtime Database เนื่องจากค่าจะขึ้นอยู่กับสถานะของไคลเอ็นต์ กล่าวคือ หากไคลเอ็นต์รายหนึ่งอ่าน /.info/connected เป็นเท็จ ก็ไม่ได้รับประกันว่าไคลเอ็นต์อีกรายหนึ่งจะอ่านเป็นเท็จด้วย

ใน Android Firebase จะจัดการสถานะการเชื่อมต่อโดยอัตโนมัติเพื่อลดแบนด์วิดท์และการใช้แบตเตอรี่ เมื่อไม่มี Listener ที่ใช้งานอยู่ ไม่มีการดำเนินการเขียนหรือ onDisconnect ที่รอดำเนินการ และไม่ได้ยกเลิกการเชื่อมต่อโดยวิธีการ goOffline อย่างชัดเจน Firebase จะปิดการเชื่อมต่อหลังจากไม่มีการใช้งานเป็นเวลา 60 วินาที

เวลาในการตอบสนองของการจัดการ

การประทับเวลาของเซิร์ฟเวอร์

เซิร์ฟเวอร์ Firebase Realtime Database มีกลไกในการแทรกการประทับเวลาที่สร้างขึ้นบนเซิร์ฟเวอร์เป็นข้อมูล ฟีเจอร์นี้เมื่อใช้ร่วมกับ onDisconnect จะช่วยให้คุณบันทึกเวลาที่ไคลเอ็นต์ Realtime Database ตัดการเชื่อมต่อได้อย่างง่ายดายและเชื่อถือได้

Kotlin+KTX

val userLastOnlineRef = Firebase.database.getReference("users/joe/lastOnline")
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)

Java

DatabaseReference userLastOnlineRef = FirebaseDatabase.getInstance().getReference("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);

ความเอียงของนาฬิกา

แม้ว่า firebase.database.ServerValue.TIMESTAMP จะแม่นยำกว่ามากและเหมาะสำหรับการดำเนินการแบบอ่าน/เขียนส่วนใหญ่ แต่บางครั้งก็อาจมีประโยชน์ในการประมาณความคลาดเคลื่อนของนาฬิกาของไคลเอ็นต์เทียบกับเซิร์ฟเวอร์ของ Firebase Realtime Database คุณสามารถแนบ Callback ไปยังตำแหน่ง /.info/serverTimeOffset เพื่อรับค่าเป็นมิลลิวินาทีที่ไคลเอ็นต์ Firebase Realtime Database บวกเวลาที่รายงานในเครื่อง (เวลา Epoch ในหน่วยมิลลิวินาที) เพื่อประมาณเวลาของเซิร์ฟเวอร์ โปรดทราบว่าความแม่นยำของออฟเซ็ตนี้อาจได้รับผลกระทบจากเวลาในการตอบสนองของเครือข่าย ดังนั้นจึงมีประโยชน์สำหรับการค้นหาความคลาดเคลื่อนขนาดใหญ่ (> 1 วินาที) ของเวลานาฬิกา

Kotlin+KTX

val offsetRef = Firebase.database.getReference(".info/serverTimeOffset")
offsetRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val offset = snapshot.getValue(Double::class.java) ?: 0.0
        val estimatedServerTimeMs = System.currentTimeMillis() + offset
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled")
    }
})

Java

DatabaseReference offsetRef = FirebaseDatabase.getInstance().getReference(".info/serverTimeOffset");
offsetRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        double offset = snapshot.getValue(Double.class);
        double estimatedServerTimeMs = System.currentTimeMillis() + offset;
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled");
    }
});

ตัวอย่างแอปการปรากฏตัว

คุณสร้างระบบการตรวจหาบุคคลในบ้านของผู้ใช้ได้โดยการรวมการดำเนินการยกเลิกการเชื่อมต่อเข้ากับการตรวจสอบสถานะการเชื่อมต่อและการประทับเวลาของเซิร์ฟเวอร์ ในระบบนี้ ผู้ใช้แต่ละรายจะเก็บข้อมูลไว้ที่ตำแหน่งฐานข้อมูลเพื่อระบุว่าไคลเอ็นต์ Realtime Database ออนไลน์อยู่หรือไม่ ไคลเอ็นต์จะตั้งค่าตำแหน่งนี้เป็น "จริง" เมื่อออนไลน์ และตั้งค่าการประทับเวลาเมื่อยกเลิกการเชื่อมต่อ การประทับเวลานี้บ่งบอกถึงเวลาที่ผู้ใช้รายดังกล่าวออนไลน์ครั้งล่าสุด

โปรดทราบว่าแอปของคุณควรจัดคิวการดำเนินการยกเลิกการเชื่อมต่อก่อนที่จะมีการทำเครื่องหมายผู้ใช้ออนไลน์ เพื่อหลีกเลี่ยงเงื่อนไขการแข่งขันในกรณีที่การเชื่อมต่อเครือข่ายของไคลเอ็นต์ขาดหายก่อนที่จะส่งทั้ง 2 คำสั่งไปยังเซิร์ฟเวอร์

ระบบการแสดงข้อมูลผู้ใช้แบบง่ายมีดังนี้

Kotlin+KTX

// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
val database = Firebase.database
val myConnectionsRef = database.getReference("users/joe/connections")

// Stores the timestamp of my last disconnect (the last time I was seen online)
val lastOnlineRef = database.getReference("/users/joe/lastOnline")

val connectedRef = database.getReference(".info/connected")
connectedRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val connected = snapshot.getValue<Boolean>() ?: false
        if (connected) {
            val con = myConnectionsRef.push()

            // When this device disconnects, remove it
            con.onDisconnect().removeValue()

            // When I disconnect, update the last time I was seen online
            lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)

            // Add this device to my connections list
            // this value could contain info about the device or a timestamp too
            con.setValue(java.lang.Boolean.TRUE)
        }
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled at .info/connected")
    }
})

Java

// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
final FirebaseDatabase database = FirebaseDatabase.getInstance();
final DatabaseReference myConnectionsRef = database.getReference("users/joe/connections");

// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/users/joe/lastOnline");

final DatabaseReference connectedRef = database.getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        boolean connected = snapshot.getValue(Boolean.class);
        if (connected) {
            DatabaseReference con = myConnectionsRef.push();

            // When this device disconnects, remove it
            con.onDisconnect().removeValue();

            // When I disconnect, update the last time I was seen online
            lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);

            // Add this device to my connections list
            // this value could contain info about the device or a timestamp too
            con.setValue(Boolean.TRUE);
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled at .info/connected");
    }
});