(वैकल्पिक) फायरबेस लोकल एमुलेटर सूट के साथ प्रोटोटाइप और परीक्षण
इस बारे में बात करने से पहले कि आपका ऐप रीयलटाइम डेटाबेस को कैसे पढ़ता और लिखता है, आइए टूल का एक सेट पेश करें जिसका उपयोग आप रीयलटाइम डेटाबेस कार्यक्षमता को प्रोटोटाइप और परीक्षण करने के लिए कर सकते हैं: फायरबेस लोकल एमुलेटर सूट। यदि आप अलग-अलग डेटा मॉडल आज़मा रहे हैं, अपने सुरक्षा नियमों को अनुकूलित कर रहे हैं, या बैक-एंड के साथ बातचीत करने का सबसे लागत प्रभावी तरीका ढूंढने के लिए काम कर रहे हैं, तो लाइव सेवाओं को तैनात किए बिना स्थानीय स्तर पर काम करने में सक्षम होना एक अच्छा विचार हो सकता है।
रीयलटाइम डेटाबेस एमुलेटर स्थानीय एमुलेटर सूट का हिस्सा है, जो आपके ऐप को आपके अनुकरणित डेटाबेस सामग्री और कॉन्फ़िगरेशन के साथ-साथ वैकल्पिक रूप से आपके अनुकरणीय प्रोजेक्ट संसाधनों (फ़ंक्शन, अन्य डेटाबेस और सुरक्षा नियमों) के साथ इंटरैक्ट करने में सक्षम बनाता है।
रीयलटाइम डेटाबेस एमुलेटर का उपयोग करने में बस कुछ ही चरण शामिल हैं:
- एम्यूलेटर से कनेक्ट करने के लिए अपने ऐप के परीक्षण कॉन्फ़िगरेशन में कोड की एक पंक्ति जोड़ना।
- आपकी स्थानीय प्रोजेक्ट निर्देशिका के मूल से,
firebase emulators:start
। - हमेशा की तरह रीयलटाइम डेटाबेस प्लेटफ़ॉर्म SDK का उपयोग करके, या रीयलटाइम डेटाबेस REST API का उपयोग करके अपने ऐप के प्रोटोटाइप कोड से कॉल करना।
रीयलटाइम डेटाबेस और क्लाउड फ़ंक्शंस से संबंधित एक विस्तृत पूर्वाभ्यास उपलब्ध है। आपको स्थानीय एमुलेटर सुइट परिचय पर भी एक नज़र डालनी चाहिए।
FIRDatabaseReference प्राप्त करें
डेटाबेस से डेटा पढ़ने या लिखने के लिए, आपको FIRDatabaseReference
का एक उदाहरण चाहिए:
तीव्र
var ref: DatabaseReference! ref = Database.database().reference()
उद्देश्य सी
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
डेटा लिखें
यह दस्तावेज़ फायरबेस डेटा को पढ़ने और लिखने की मूल बातें शामिल करता है।
फायरबेस डेटा को Database
संदर्भ में लिखा जाता है और संदर्भ में एक एसिंक्रोनस श्रोता को जोड़कर पुनर्प्राप्त किया जाता है। श्रोता को डेटा की प्रारंभिक स्थिति के लिए एक बार ट्रिगर किया जाता है और फिर जब भी डेटा बदलता है।
बुनियादी लेखन संचालन
बुनियादी लेखन कार्यों के लिए, आप उस पथ पर किसी भी मौजूदा डेटा को प्रतिस्थापित करके, निर्दिष्ट संदर्भ में डेटा को सहेजने के लिए setValue
उपयोग कर सकते हैं। आप इस विधि का उपयोग इसके लिए कर सकते हैं:
- उपलब्ध JSON प्रकारों के अनुरूप प्रकार पास करें:
-
NSString
-
NSNumber
-
NSDictionary
-
NSArray
-
उदाहरण के लिए, आप setValue
के साथ एक उपयोगकर्ता को इस प्रकार जोड़ सकते हैं:
तीव्र
self.ref.child("users").child(user.uid).setValue(["username": username])
उद्देश्य सी
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
इस तरह से setValue
उपयोग किसी भी चाइल्ड नोड सहित निर्दिष्ट स्थान पर डेटा को ओवरराइट कर देता है। हालाँकि, आप अभी भी संपूर्ण ऑब्जेक्ट को दोबारा लिखे बिना किसी बच्चे को अपडेट कर सकते हैं। यदि आप उपयोगकर्ताओं को अपनी प्रोफ़ाइल अपडेट करने की अनुमति देना चाहते हैं तो आप निम्नानुसार उपयोगकर्ता नाम अपडेट कर सकते हैं:
तीव्र
self.ref.child("users/\(user.uid)/username").setValue(username)
उद्देश्य सी
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
डेटा पढ़ें
मूल्य घटनाओं को सुनकर डेटा पढ़ें
किसी पथ पर डेटा पढ़ने और परिवर्तनों को सुनने के लिए, FIRDatabaseReference
के observeEventType:withBlock
का उपयोग FIRDataEventTypeValue
घटनाओं का निरीक्षण करने के लिए करें।
घटना प्रकार | विशिष्ट उपयोग |
---|---|
FIRDataEventTypeValue | किसी पथ की संपूर्ण सामग्री में परिवर्तन पढ़ें और सुनें। |
आप किसी दिए गए पथ पर डेटा पढ़ने के लिए FIRDataEventTypeValue
ईवेंट का उपयोग कर सकते हैं, क्योंकि यह ईवेंट के समय मौजूद होता है। श्रोता के संलग्न होने पर यह विधि एक बार चालू हो जाती है और हर बार किसी भी बच्चे सहित डेटा में परिवर्तन होता है। इवेंट कॉलबैक में एक snapshot
भेजा जाता है जिसमें चाइल्ड डेटा सहित उस स्थान का सारा डेटा होता है। यदि कोई डेटा नहीं है, तो जब आप exists()
कॉल करते हैं तो स्नैपशॉट false
लौटाएगा और जब आप इसकी value
प्रॉपर्टी पढ़ेंगे तो nil
।
निम्नलिखित उदाहरण एक सामाजिक ब्लॉगिंग एप्लिकेशन को डेटाबेस से किसी पोस्ट का विवरण पुनर्प्राप्त करते हुए दर्शाता है:
तीव्र
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
उद्देश्य सी
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
श्रोता को एक FIRDataSnapshot
प्राप्त होता है जिसमें घटना के समय डेटाबेस में निर्दिष्ट स्थान पर उसके value
गुण में डेटा शामिल होता है। आप उचित मूल प्रकार, जैसे NSDictionary
, को मान निर्दिष्ट कर सकते हैं। यदि स्थान पर कोई डेटा मौजूद नहीं है, तो value
nil
है।
एक बार डेटा पढ़ें
GetData() का उपयोग करके एक बार पढ़ें
एसडीके को डेटाबेस सर्वर के साथ इंटरैक्शन प्रबंधित करने के लिए डिज़ाइन किया गया है, चाहे आपका ऐप ऑनलाइन हो या ऑफलाइन।
आम तौर पर, आपको बैकएंड से डेटा के अपडेट की सूचना प्राप्त करने के लिए डेटा को पढ़ने के लिए ऊपर वर्णित वैल्यू इवेंट तकनीकों का उपयोग करना चाहिए। वे तकनीकें आपके उपयोग और बिलिंग को कम करती हैं, और आपके उपयोगकर्ताओं को ऑनलाइन और ऑफलाइन होने पर सर्वोत्तम अनुभव देने के लिए अनुकूलित की जाती हैं।
यदि आपको केवल एक बार डेटा की आवश्यकता है, तो आप डेटाबेस से डेटा का स्नैपशॉट प्राप्त करने के लिए getData()
उपयोग कर सकते हैं। यदि किसी भी कारण से getData()
सर्वर मान वापस करने में असमर्थ है, तो क्लाइंट स्थानीय स्टोरेज कैश की जांच करेगा और यदि मान अभी भी नहीं मिला है तो एक त्रुटि लौटाएगा।
निम्नलिखित उदाहरण डेटाबेस से उपयोगकर्ता के सार्वजनिक-सामना वाले उपयोगकर्ता नाम को एक बार पुनः प्राप्त करना दर्शाता है:
तीव्र
ref.child("users/\(uid)/username").getData(completion: { error, snapshot in guard error == nil else { print(error!.localizedDescription) return; } let userName = snapshot.value as? String ?? "Unknown"; });
उद्देश्य सी
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid]; [[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) { if (error) { NSLog(@"Received an error %@", error); return; } NSString *userName = snapshot.value; }];
getData()
के अनावश्यक उपयोग से बैंडविड्थ का उपयोग बढ़ सकता है और प्रदर्शन में कमी आ सकती है, जिसे ऊपर दिखाए अनुसार रीयलटाइम श्रोता का उपयोग करके रोका जा सकता है।
किसी पर्यवेक्षक के साथ एक बार डेटा पढ़ें
कुछ मामलों में आप सर्वर पर अद्यतन मूल्य की जाँच करने के बजाय, स्थानीय कैश से मूल्य तुरंत वापस करना चाह सकते हैं। उन मामलों में आप स्थानीय डिस्क कैश से तुरंत डेटा प्राप्त करने के लिए observeSingleEventOfType
उपयोग कर सकते हैं।
यह उस डेटा के लिए उपयोगी है जिसे केवल एक बार लोड करने की आवश्यकता होती है और जिसके बार-बार बदलने या सक्रिय रूप से सुनने की आवश्यकता नहीं होती है। उदाहरण के लिए, पिछले उदाहरणों में ब्लॉगिंग ऐप किसी उपयोगकर्ता की प्रोफ़ाइल को लोड करने के लिए इस पद्धति का उपयोग करता है जब वे एक नई पोस्ट लिखना शुरू करते हैं:
तीव्र
let userID = Auth.auth().currentUser?.uid ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in // Get user value let value = snapshot.value as? NSDictionary let username = value?["username"] as? String ?? "" let user = User(username: username) // ... }) { error in print(error.localizedDescription) }
उद्देश्य सी
NSString *userID = [FIRAuth auth].currentUser.uid; [[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { // Get user value User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]]; // ... } withCancelBlock:^(NSError * _Nonnull error) { NSLog(@"%@", error.localizedDescription); }];
डेटा अपडेट करना या हटाना
विशिष्ट फ़ील्ड अपडेट करें
अन्य चाइल्ड नोड्स को अधिलेखित किए बिना एक नोड के विशिष्ट बच्चों को एक साथ लिखने के लिए, updateChildValues
विधि का उपयोग करें।
updateChildValues
कॉल करते समय, आप कुंजी के लिए पथ निर्दिष्ट करके निचले स्तर के चाइल्ड मानों को अपडेट कर सकते हैं। यदि बेहतर पैमाने पर डेटा को कई स्थानों पर संग्रहीत किया जाता है, तो आप डेटा फैन-आउट का उपयोग करके उस डेटा के सभी उदाहरणों को अपडेट कर सकते हैं। उदाहरण के लिए, एक सोशल ब्लॉगिंग ऐप एक पोस्ट बनाना चाहता है और साथ ही उसे हालिया गतिविधि फ़ीड और पोस्ट करने वाले उपयोगकर्ता की गतिविधि फ़ीड में अपडेट करना चाहता है। ऐसा करने के लिए, ब्लॉगिंग एप्लिकेशन इस तरह कोड का उपयोग करता है:
तीव्र
guard let key = ref.child("posts").childByAutoId().key else { return } let post = ["uid": userID, "author": username, "title": title, "body": body] let childUpdates = ["/posts/\(key)": post, "/user-posts/\(userID)/\(key)/": post] ref.updateChildValues(childUpdates)
उद्देश्य सी
NSString *key = [[_ref child:@"posts"] childByAutoId].key; NSDictionary *post = @{@"uid": userID, @"author": username, @"title": title, @"body": body}; NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post, [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post}; [_ref updateChildValues:childUpdates];
यह उदाहरण /posts/$postid
पर सभी उपयोगकर्ताओं के लिए पोस्ट युक्त नोड में एक पोस्ट बनाने के लिए childByAutoId
उपयोग करता है और साथ ही getKey()
के साथ कुंजी पुनर्प्राप्त करता है। कुंजी का उपयोग उपयोगकर्ता की पोस्ट में /user-posts/$userid/$postid
पर दूसरी प्रविष्टि बनाने के लिए किया जा सकता है।
इन पथों का उपयोग करके, आप updateChildValues
पर एक कॉल के साथ JSON ट्री में कई स्थानों पर एक साथ अपडेट कर सकते हैं, जैसे कि यह उदाहरण दोनों स्थानों में नई पोस्ट कैसे बनाता है। इस तरह से किए गए एक साथ किए गए अपडेट परमाणु होते हैं: या तो सभी अपडेट सफल होते हैं या सभी अपडेट विफल हो जाते हैं।
एक समापन ब्लॉक जोड़ें
यदि आप जानना चाहते हैं कि आपका डेटा कब प्रतिबद्ध किया गया है, तो आप एक पूर्णता ब्लॉक जोड़ सकते हैं। setValue
और updateChildValues
दोनों एक वैकल्पिक पूर्णता ब्लॉक लेते हैं जिसे तब कहा जाता है जब लेखन डेटाबेस के लिए प्रतिबद्ध होता है। यह श्रोता यह ट्रैक रखने के लिए उपयोगी हो सकता है कि कौन सा डेटा सहेजा गया है और कौन सा डेटा अभी भी सिंक्रनाइज़ किया जा रहा है। यदि कॉल असफल होती है, तो श्रोता को एक त्रुटि ऑब्जेक्ट भेजा जाता है जो बताता है कि विफलता क्यों हुई।
तीव्र
ref.child("users").child(user.uid).setValue(["username": username]) { (error:Error?, ref:DatabaseReference) in if let error = error { print("Data could not be saved: \(error).") } else { print("Data saved successfully!") } }
उद्देश्य सी
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { if (error) { NSLog(@"Data could not be saved: %@", error); } else { NSLog(@"Data saved successfully."); } }];
डेटा हटाएँ
डेटा को हटाने का सबसे सरल तरीका उस डेटा के स्थान के संदर्भ में removeValue
कॉल करना है।
आप किसी अन्य लेखन ऑपरेशन जैसे setValue
या updateChildValues
के मान के रूप में nil
निर्दिष्ट करके भी हटा सकते हैं। आप एक ही एपीआई कॉल में एकाधिक बच्चों को हटाने के लिए updateChildValues
के साथ इस तकनीक का उपयोग कर सकते हैं।
श्रोताओं को अलग करें
जब आप ViewController
छोड़ते हैं तो पर्यवेक्षक स्वचालित रूप से डेटा सिंक करना बंद नहीं करते हैं। यदि किसी पर्यवेक्षक को ठीक से नहीं हटाया जाता है, तो यह डेटा को स्थानीय मेमोरी में सिंक करना जारी रखता है। जब किसी पर्यवेक्षक की आवश्यकता नहीं रह जाती है, तो संबंधित FIRDatabaseHandle
removeObserverWithHandle
विधि से पास करके इसे हटा दें।
जब आप किसी संदर्भ में कॉलबैक ब्लॉक जोड़ते हैं, तो एक FIRDatabaseHandle
वापस आ जाता है। इन हैंडल का उपयोग कॉलबैक ब्लॉक को हटाने के लिए किया जा सकता है।
यदि डेटाबेस संदर्भ में एकाधिक श्रोताओं को जोड़ा गया है, तो किसी घटना को उठाए जाने पर प्रत्येक श्रोता को कॉल किया जाता है। उस स्थान पर डेटा सिंक करना बंद करने के लिए, आपको removeAllObservers
विधि को कॉल करके किसी स्थान पर सभी पर्यवेक्षकों को हटाना होगा।
किसी श्रोता पर removeObserverWithHandle
या removeAllObservers
कॉल करने से उसके चाइल्ड नोड्स पर पंजीकृत श्रोता स्वचालित रूप से नहीं हटते हैं; आपको उन संदर्भों या हैंडलों को हटाने के लिए उन पर भी नज़र रखनी होगी।
लेनदेन के रूप में डेटा सहेजें
डेटा के साथ काम करते समय जो समवर्ती संशोधनों, जैसे वृद्धिशील काउंटरों द्वारा दूषित हो सकता है, आप लेनदेन ऑपरेशन का उपयोग कर सकते हैं। आप इस ऑपरेशन को दो तर्क देते हैं: एक अद्यतन फ़ंक्शन और एक वैकल्पिक पूर्णता कॉलबैक। अद्यतन फ़ंक्शन डेटा की वर्तमान स्थिति को एक तर्क के रूप में लेता है और वह नई वांछित स्थिति लौटाता है जिसे आप लिखना चाहते हैं।
उदाहरण के लिए, उदाहरण के सोशल ब्लॉगिंग ऐप में, आप उपयोगकर्ताओं को पोस्ट को स्टार और अनस्टार करने की अनुमति दे सकते हैं और इस प्रकार ट्रैक कर सकते हैं कि किसी पोस्ट को कितने स्टार मिले हैं:
तीव्र
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in if var post = currentData.value as? [String: AnyObject], let uid = Auth.auth().currentUser?.uid { var stars: [String: Bool] stars = post["stars"] as? [String: Bool] ?? [:] var starCount = post["starCount"] as? Int ?? 0 if let _ = stars[uid] { // Unstar the post and remove self from stars starCount -= 1 stars.removeValue(forKey: uid) } else { // Star the post and add self to stars starCount += 1 stars[uid] = true } post["starCount"] = starCount as AnyObject? post["stars"] = stars as AnyObject? // Set value and report transaction success currentData.value = post return TransactionResult.success(withValue: currentData) } return TransactionResult.success(withValue: currentData) }) { error, committed, snapshot in if let error = error { print(error.localizedDescription) } }
उद्देश्य सी
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { NSMutableDictionary *post = currentData.value; if (!post || [post isEqual:[NSNull null]]) { return [FIRTransactionResult successWithValue:currentData]; } NSMutableDictionary *stars = post[@"stars"]; if (!stars) { stars = [[NSMutableDictionary alloc] initWithCapacity:1]; } NSString *uid = [FIRAuth auth].currentUser.uid; int starCount = [post[@"starCount"] intValue]; if (stars[uid]) { // Unstar the post and remove self from stars starCount--; [stars removeObjectForKey:uid]; } else { // Star the post and add self to stars starCount++; stars[uid] = @YES; } post[@"stars"] = stars; post[@"starCount"] = @(starCount); // Set value and report transaction success currentData.value = post; return [FIRTransactionResult successWithValue:currentData]; } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { // Transaction completed if (error) { NSLog(@"%@", error.localizedDescription); } }];
यदि कई उपयोगकर्ता एक ही समय में एक ही पोस्ट को स्टार करते हैं या क्लाइंट के पास पुराना डेटा है तो लेनदेन का उपयोग करने से स्टार काउंट गलत होने से बच जाता है। FIRMutableData
वर्ग में निहित मान प्रारंभ में पथ के लिए क्लाइंट का अंतिम ज्ञात मान है, या यदि कोई नहीं है तो nil
। सर्वर प्रारंभिक मूल्य की तुलना उसके वर्तमान मूल्य से करता है और यदि मूल्य मेल खाते हैं तो लेनदेन को स्वीकार करता है, या इसे अस्वीकार कर देता है। यदि लेनदेन अस्वीकार कर दिया जाता है, तो सर्वर क्लाइंट को वर्तमान मूल्य लौटाता है, जो अद्यतन मूल्य के साथ लेनदेन को फिर से चलाता है। यह तब तक दोहराया जाता है जब तक कि लेनदेन स्वीकार नहीं हो जाता या बहुत अधिक प्रयास नहीं किए जाते।
परमाणु सर्वर-साइड वृद्धि
उपरोक्त उपयोग के मामले में हम डेटाबेस में दो मान लिख रहे हैं: उस उपयोगकर्ता की आईडी जो पोस्ट को स्टार/अनस्टार करता है, और बढ़ी हुई स्टार गिनती। यदि हम पहले से ही जानते हैं कि उपयोगकर्ता पोस्ट को अभिनीत कर रहा है, तो हम लेनदेन के बजाय परमाणु वृद्धि ऑपरेशन का उपयोग कर सकते हैं।
तीव्र
let updates = [ "posts/\(postID)/stars/\(userID)": true, "posts/\(postID)/starCount": ServerValue.increment(1), "user-posts/\(postID)/stars/\(userID)": true, "user-posts/\(postID)/starCount": ServerValue.increment(1) ] as [String : Any] Database.database().reference().updateChildValues(updates);
उद्देश्य सी
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1], [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]}; [[[FIRDatabase database] reference] updateChildValues:updates];
यह कोड लेन-देन ऑपरेशन का उपयोग नहीं करता है, इसलिए यदि कोई विरोधाभासी अद्यतन होता है तो यह स्वचालित रूप से दोबारा नहीं चलता है। हालाँकि, चूंकि वेतन वृद्धि कार्रवाई सीधे डेटाबेस सर्वर पर होती है, इसलिए विरोध की कोई संभावना नहीं है।
यदि आप एप्लिकेशन-विशिष्ट विरोधों का पता लगाना और उन्हें अस्वीकार करना चाहते हैं, जैसे कि किसी उपयोगकर्ता द्वारा किसी पोस्ट को तारांकित करना जिसे उन्होंने पहले ही तारांकित कर दिया है, तो आपको उस उपयोग के मामले के लिए कस्टम सुरक्षा नियम लिखना चाहिए।
डेटा के साथ ऑफ़लाइन कार्य करें
यदि कोई क्लाइंट अपना नेटवर्क कनेक्शन खो देता है, तो आपका ऐप सही ढंग से काम करता रहेगा।
फायरबेस डेटाबेस से जुड़ा प्रत्येक क्लाइंट किसी भी सक्रिय डेटा का अपना आंतरिक संस्करण रखता है। जब डेटा लिखा जाता है, तो उसे पहले इस स्थानीय संस्करण में लिखा जाता है। फ़ायरबेस क्लाइंट तब उस डेटा को दूरस्थ डेटाबेस सर्वर और अन्य क्लाइंट के साथ "सर्वोत्तम प्रयास" के आधार पर सिंक्रनाइज़ करता है।
परिणामस्वरूप, सर्वर पर कोई भी डेटा लिखे जाने से पहले, डेटाबेस पर लिखी जाने वाली सभी चीज़ें तुरंत स्थानीय घटनाओं को ट्रिगर करती हैं। इसका मतलब है कि आपका ऐप नेटवर्क विलंबता या कनेक्टिविटी की परवाह किए बिना प्रतिक्रियाशील बना रहता है।
एक बार कनेक्टिविटी पुनः स्थापित हो जाने पर, आपके ऐप को ईवेंट का उचित सेट प्राप्त होता है ताकि क्लाइंट बिना कोई कस्टम कोड लिखे वर्तमान सर्वर स्थिति के साथ समन्वयित हो सके।
हम ऑनलाइन और ऑफ़लाइन क्षमताओं के बारे में और जानें में ऑफ़लाइन व्यवहार के बारे में अधिक बात करेंगे।