WhatsApp ইঞ্জিনিয়ারদের থেকে সরাসরি শুনুন যে তারা কীভাবে তাদের Wear OS অ্যাপকে স্কেলে আপগ্রেড করেছেন। এই সেশনে Material 3 ইন্টিগ্রেট করা এবং Wear উইজেট গ্রহণ করার পিছনে আর্কিটেকচারাল এবং UX সংক্রান্ত সিদ্ধান্তগুলি কভার করা হয়েছে।
টিম তাদের অ্যাপ আপডেট করতে এবং একটি সমন্বিত ব্যবহারকারীর অভিজ্ঞতা প্রদান করতে যে পাঠ, প্যাটার্ন এবং টুল ব্যবহার করেছে সে সম্পর্কে সরাসরি ইনসাইট পান।
TransformingLazyColumn সঠিকভাবে ব্যবহার করার অর্থ হল শুধুমাত্র একটি লেভেলে ট্রান্সফর্মেশন প্রয়োগ করা এবং Compose-এর remember-এর মাধ্যমে পজিশন ক্যাশে করা এড়িয়ে চলা।ট্রান্সস্ক্রিপ্ট করা
হ্যালো।
আমার নাম মার্টিন।
আর আমি সুভাগ।
আমরা Meta-র ইঞ্জিনিয়ার এবং Wear OS-এ WhatsApp-এর পিছনে যে টিম কাজ করে আমরা তার সদস্য।
এবং আমরা সম্প্রতি আমাদের অ্যাপকে আধুনিক করেছি।
তাই আজ, আমরা শেয়ার করা কোডবেসে মাইগ্রেট করার সময় এবং তারপরে এর উপরে নতুন অভিজ্ঞতা তৈরি করার সময় যে শিক্ষা পেয়েছি তা শেয়ার করতে চলেছি।
Wear OS-এ WhatsApp-এর সবচেয়ে আকর্ষণীয় দিক হল, এটি আপনার ফোন ছাড়াই কাজ করে।
লিঙ্ক করা হয়ে গেলে, এটি নিজস্ব WhatsApp ডিভাইস হিসেবে কাজ করে, আপনার ফোনের মতোই এন্ড-টু-এন্ড এনক্রিপ্টেড, নিজস্ব ডেটাবেস এবং সার্ভারের সাথে কানেকশন সহ।
তাই আপনার ফোন রেঞ্জের বাইরে থাকলেও এটি ওয়াই-ফাই বা LTE-তে কাজ করতে থাকে।
আমরা ২০২৩ সালে প্রথমবার Wear OS-এ WhatsApp লঞ্চ করেছি।
এটি Rust-এ লেখা একটি কম্প্যাক্ট, পোর্টেবল ক্লায়েন্ট দ্বারা পরিচালিত হয়েছিল।
এর ফলে আমরা দ্রুত প্রোডাকশন শুরু করতে পেরেছি।
তবে, এটি ছিল WhatsApp-এর দ্বিতীয়, স্বতন্ত্র ইমপ্লিমেন্টেশন, যার অর্থ হল প্রতিটি প্রোটোকল, এনক্রিপশন এবং ফিচার দুবার তৈরি করতে হয়েছিল।
তাই ওয়াচ অ্যাপ সবসময় ক্যাচ আপ করছিল।
পোর্টেবল হওয়ার কারণে ব্যাকগ্রাউন্ড ম্যানেজ করাও কঠিন হয়ে পড়ে।
ক্লায়েন্ট নিজেই থ্রেড, টাইমার ও সকেট ম্যানেজ করে, যেগুলি Android আপনার জন্য ম্যানেজ করতে চায়।
Android-এ ব্যাকগ্রাউন্ডের জন্য নিয়ম আছে।
ডোজ মোডে থাকলে, আপনি নেটওয়ার্ক হারান।
আপনার প্রসেস ক্যাশে করা হয়ে গেলে, আপনার কাছে সীমিত বা কোনও এক্সিকিউশন টাইম থাকে না।
এটি কাটিয়ে উঠতে আপনার কোনও পরিষেবা, কাজ বা পুশ প্রয়োজন।
তাই আমাদের ওয়াচ অ্যাপকে সেগুলি ম্যানুয়ালি কানেক্ট করতে হয়েছে।
কিন্তু আমাদের Android কোডবেস আগে থেকেই তা করে।
তাই আমরা আমাদের Android কোডবেস আবার ব্যবহার ও মাইগ্রেট করার সিদ্ধান্ত নিয়েছি, যা কোটি কোটি ডিভাইসে পরীক্ষা করা হয়েছে এবং আগে থেকেই Android-এর নিয়ম মেনে চলে।
এর ফলে, নতুন ফিচার দু'বার তৈরি করতে হয় না।
লক্ষ্যটি সহজ ছিল: ব্যবসার লজিক রেখে UI সরিয়ে দেওয়া।
যা বাকি আছে তা হল একটি হেডলেস লাইব্রেরি যা Kotlin ফ্লোকে এর API হিসেবে ব্যবহার করে।
তবে, চ্যালেঞ্জ হল, আপনি একবারে এই সাইজের অ্যাপ আনট্যাঙ্গেল করতে পারবেন না।
কিছু মডিউল UI-এর সাথে ইনফ্রা মিক্স করে।
অন্যান্য ফিচার Wear OS অ্যাপে আপনার প্রয়োজন নেই এমন ফিচারের উপর নির্ভর করে।
তাই আমরা দুটি ট্র্যাকে কাজ করেছি: আমরা মডিউলারাইজ করা চালিয়ে গেছি এবং আমরা যা এখনও সরাতে পারিনি তা স্ট্রিপ বা ওভার-ওভাররাইট করেছি।
মডিউলারাইজেশন দিয়ে শুরু করা যাক।
বিল্ড থেকে কোড বাদ দিতে, "এটি ফোন অ্যাপ হলে" স্টেটমেন্ট কাজ করবে না কারণ এটি কোড সরিয়ে দেয় না।
তাই আমরা DI-এর উপর খুব বেশি নির্ভর করি, বিশেষ করে ঐচ্ছিক এবং মাল্টিবাইন্ডিং।
আমরা ইন-হাউস DI ব্যবহার করি, তবে এটি Dagger যা অফার করে তার সাথে ঘনিষ্ঠভাবে ম্যাপ করে।
তাই উদাহরণগুলি ড্যাগারে রয়েছে।
ঐচ্ছিক বাইন্ডিংয়ের মাধ্যমে, Dagger-এ এটি হল BindsOptionalOf, আপনি ডিপেন্ডেন্সি ঐচ্ছিক হিসেবে ঘোষণা করতে পারেন, যেমন ব্যাকএন্ড কল করা।
@Module
interface CallingModule {
@BindsOptionalOf
fun callingBackend(): CallingBackend
}
class Calls @Inject constructor(
val backend: Optional<CallingBackend>,
) {
fun start(): CallResult =
backend.getOrNull()?.start()
?: CallResult.NotAllowed
}তারপর, যদি ওয়াচ বিল্ডে এমন একটি মডিউল অন্তর্ভুক্ত থাকে যা ইমপ্লিমেন্টেশন প্রদান করে, তাহলে ইনজেক্ট করা ব্যাকএন্ড উপস্থিত থাকবে এবং কলিং কাজ করবে।
আপনি মডিউলটি বাদ দিলে, এটি শূন্য হবে এবং আপনার API NotAllowed রিটার্ন করতে পারে।
মাল্টিবাইন্ডিং একই কাজ করে, তবে সেটের জন্য।
@Module
interface CoreModule {
@Multibinds
fun handlers(): Set<MessageHandler>
}
@Module
interface ReactionsModule {
@Binds @IntoSet
fun reactions(impl: ReactionHandler):
MessageHandler
}
class MessageDispatcher @Inject constructor(
private val handlers: Set<MessageHandler>,
)আপনি multibinds-এ একটি সেট ঘোষণা করতে পারেন, যেমন MessageHandler এবং তারপর প্রতিটি ফিচার মডিউল সেট মাল্টিবাইন্ডিং-এ ব্যবহার করে নিজস্ব হ্যান্ডলার যোগ করতে পারে।
আপনি যদি পেমেন্ট সংক্রান্ত সুবিধা দিতে না চান, যেমন আপনি বিল্ড থেকে সম্পূর্ণ পেমেন্ট মডিউল বাদ দিতে পারেন এবং ইনজেক্ট করা সেটে সেই হ্যান্ডলার অন্তর্ভুক্ত থাকবে না।
সেইভাবে, ফিচার কাজ করে কিনা তা বিল্ড গ্রাফের জন্য প্রশ্ন।
তবে, এই ডিকাপলিংয়ের সুবিধা ও অসুবিধা দুই-ই আছে।
সরাসরি রেফারেন্সের ক্ষেত্রে, কোনও মডিউল মুছে দিলে আপনার বিল্ড ভেঙে যাবে।
ঐচ্ছিক বা মাল্টিবাইন্ডিংয়ের ক্ষেত্রে, মডিউলকে রেফারেন্স করে এমন কিছু নেই, তাই বিল্ড পাস হয়ে যায় এবং ফিচারটি নীরবে অদৃশ্য হয়ে যায় বা ডিফল্ট টেক জায়গা করে নেয়।
আর আমরা ঠিক এটাই করেছি।
রিফ্যাক্টর একটি ফিচারকে তার নিজস্ব মডিউলে সরিয়ে দিয়েছে এবং আমাদের বিল্ড অ্যাপ এবং আমাদের ওয়াচ অ্যাপে এটি অন্তর্ভুক্ত করা হয়নি।
এর ফলে ভয়েস মেসেজের সময়সীমা শূন্য সেকেন্ড হিসেবে দেখানো হয়েছে।
তাহলে, এটি যাতে না ঘটে তা কীভাবে নিশ্চিত করবেন?
ঠিক আছে, চেকটি আবার চালু করতে আপনাকে একটি পরীক্ষা করতে হবে।
আমাদের ক্ষেত্রে, আমরা যা করি তা হল গুরুত্বপূর্ণ ইন্টারফেসের জন্য ফোন অ্যাপ এবং ওয়াচ অ্যাপের মধ্যে মাল্টিবাইন্ডিং সেট তুলনা করি।
এইভাবে আমরা নিশ্চিত করি যে আমাদের কাছে সঠিক ইমপ্লিমেন্টেশন সবসময় বাউন্ড করা আছে।
এখন ট্র্যাক দুই কভার করে যা আমরা এখনও সরাতে পারিনি।
মডিউল আবার ব্যবহার করলে আপনার কাজ বাঁচবে, তবে তারা যা কিছু ঘোষণা করে তা আপনার ম্যানিফেস্টে মার্জ হয়ে যায়।
ফোন লঞ্চার অ্যাক্টিভিটি সেকেন্ডারি অ্যাপ আইকন যোগ করবে অথবা আপনার অ্যাপের সাথে অপ্রয়োজনীয় কন্টেন্ট প্রোভাইডার শুরু হবে।
ম্যানিফেস্ট মার্জার একটি উপযোগী tools:node="remove" অ্যাট্রিবিউট অফার করে যা আপনাকে কম্পোনেন্টগুলি বের করে নিতে দেয়।
<!-- remove unused Main activity node -->
<activity android:name="com.whatsapp.Main"
tools:node="remove" />
<!-- redirect any calls to a new Activity -->
<activity-alias
android:name="com.whatsapp.Main"
android:targetActivity=".MyNewActivity" />
<provider
android:name="…MigrationContentProvider"
tools:node="remove" />এবং একবার তারা বেরিয়ে গেলে, সঙ্কুচিত কোডটিও সরিয়ে ফেলতে পারে।
এখনও যদি কোনও কিছু অ্যাক্টিভিটিতে পৌঁছাতে হয়, তাহলে আপনি নতুন ক্লাসে রিডাইরেক্ট করতে activity-alias ব্যবহার করতে পারেন।
তবে, মনে রাখবেন যে এইসব অ্যাট্রিবিউট শুধুমাত্র কম-অগ্রাধিকারের ম্যানিফেস্টে প্রযোজ্য হয়, তাই মার্জ করার ক্রম নির্ধারণ করে যে কোনও কিছু সরানো হবে কিনা।
রিসোর্স একইভাবে কাজ করে।
Reuse আপনাকে ফ্রিতে ফোন স্ট্রিং দেয়।
তবে, কিছু কিছু অ্যাপ এমন সব প্রতিশ্রুতি দেয় যা স্মার্টওয়াচ করতে পারে না।
প্রতিটি চ্যাটের শুরুতে যে এনক্রিপশন বিজ্ঞপ্তি থাকে, তার শেষে একটি উদাহরণ দেখা যাক।
ফোনে, এটি "আরও জানুন" লিঙ্কের মাধ্যমে শেষ হয়।
তবে, স্মার্টওয়াচে লিঙ্কের পিছনে কিছুই নেই।
তাই আমরা যা করি তা হল, আমরা ঘড়িতে উচ্চ-অগ্রাধিকার মডিউলে একই কী আবার ঘোষণা করি এবং স্ট্রিংটি "আরও জানতে আপনার ফোন ব্যবহার করুন" দিয়ে প্রতিস্থাপন করি।
<!-- phone -->
<string name="…_disclosure">… Learn more</string>
<!-- wearable override, same key -->
<string name="…_disclosure">…Use your phone to learn more.</string>এবং তারপর রিসোর্স মার্জার আমাদেরটা বেছে নেয়।
এছাড়াও, আপনি যাতে ভুলবশত স্ট্রিং ওভাররাইড না করে ফেলেন তা নিশ্চিত করার জন্য আপনার কিছু প্রয়োজন।
তাই আমরা বিল্ড রুল তৈরি করেছি যা নিশ্চিত করে এবং ওভাররাইড করার অনুমতি তালিকায় না থাকলে ব্যর্থ হয়।
এবার বিজ্ঞপ্তি সম্পর্কে কথা বলা যাক।
ডিফল্ট হিসেবে, Wear OS আপনার ফোনের বিজ্ঞপ্তি ব্রিজ বা মিরর করে।
খুব ভালো, তবে আপনার ফোন কানেক্ট করা থাকলে তবেই এটি কাজ করবে।
এবং আমরা চাই WhatsApp স্বাধীনভাবে কাজ করুক, তাই আমাদের অ্যাপ নিজস্ব বিজ্ঞপ্তি পোস্ট করে।
কিন্তু ব্রিজিং চালু থাকলে, প্রতিটি মেসেজ দুবার করে বাজবে।
ব্রিজিং বন্ধ করলে ডুপ্লিকেট সংক্রান্ত সমস্যার সমাধান হয়।
একটি স্ট্যাটিক ম্যানিফেস্ট ফ্ল্যাগ আছে, তবে ব্যবহারকারী লগ-ইন করার আগে ইনস্টল করার সময় থেকে এটি প্রযোজ্য হয়।
তাই আপনি যদি ইনস্টল করেন এবং লগ-ইন না করেন, তাহলে আপনি হঠাৎ সব বিজ্ঞপ্তি হারাবেন, যা ভালো নয়, তাই না?
তাই আমরা রানটাইমে BridgingManager ব্যবহার করে এটি সেট করি।
isRegistered.collect {
when (it) {
// watch posts its own
true -> bridging.setEnabled(false)
// phone bridges again
false -> bridging.setEnabled(true)
}
}
fun setEnabled(bridgingEnabled){
bridgingManager.setConfig(
BridgingConfig.Builder(context,
bridgingEnabled)
.build())
}আপনি লগ-ইন করলে, আমরা ব্রিজিং বন্ধ করে দিই।
আপনি লগ-আউট করলে, আমরা এটি আবার চালু করে দিই।
তবে, মনে রাখবেন যে ভ্যালু আবার পড়ার জন্য কোনও API নেই এবং আমরা লক্ষ্য করেছি যে স্মার্টওয়াচ এটি হারিয়ে ফেলে।
তাই, চার্জার থেকে স্মার্টওয়াচ সরিয়ে নিলে বা অ্যাপ আপডেট হলে আমরা এটি আবার লিখি।
শেষ যে বিষয়টি নিয়ে আমি আলোচনা করতে চেয়েছিলাম তা হল, তুমি কীভাবে লগ-আউট করো?
আপনার অ্যাকাউন্ট থেকে কোনও স্মার্টওয়াচ আনলিঙ্ক করলে, লোকজন আশা করেন যে মেসেজও তার সাথে চলে যাবে।
তাই আমরা সবকিছু মুছে দিই-- মেসেজ, কী, সেটিংস।
কিন্তু কীভাবে নিরাপদে তা করা যায়?
ব্যাকগ্রাউন্ড থ্রেড এখনও এতে লেখার সময় ফাইলগুলি মুছে ফেলুন, আপনি SQLite ব্যতিক্রম পাবেন, বা আরও খারাপ, নতুন তৈরি করা ডেটাবেসে লিখুন।
এবং আপনি কোনও প্রসেস না থামিয়ে সেটির স্টেট মুছে দিতে পারবেন না।
Android আপনাকে clearApplicationUserData API অফার করে, যা ডেটা মুছে দেয় এবং অ্যাপ বন্ধ করে দেয়, তবে এটি অনুমতিও রিসেট করে।
কিছু OEM-এ, বিজ্ঞপ্তি ব্রিজিং বিজ্ঞপ্তির অনুমতির সাথে যুক্ত থাকে, তাই আপনি এটি ব্যবহার করলে, আপনি হঠাৎ ফোন মিররিংও হারাবেন।
তাই আমরা নিজেরাই প্রসেসটি বন্ধ করে দিই।
clearActivityBackStack()
// enumerate our own, kill each
killOtherProcesses()
// no threads alive — now safe
deleteDataDirectories()
// show logged out confirmation
firePostCleanupIntent()আমরা সেকেন্ডারি প্রসেসে একটি ছোট পরিষেবা তৈরি করি, যার কোনও DI, কোনও ওয়ার্ক ম্যানেজার নেই, তাই এটি অ্যাপের বাকি অংশ শুরু করে না।
আপনি লগ-আউট করার পরে, এই পরিষেবা আমাদের অ্যাপের অন্যান্য সব প্রসেস বন্ধ করে দেয় এবং তারপরেই সব ডেটা মুছে দেয় যখন কোনও কিছু এটিকে রেস করছে না।
যেহেতু আমরা clearApplicationUserData ব্যবহার করি না, তাই অনুমতিগুলি থেকে যায় এবং আমরা বিজ্ঞপ্তি ব্রিজিং চালু করতে পারি, যাতে ফোনের বিজ্ঞপ্তিগুলি ফিরে আসে।
প্রথম অংশ এখানেই শেষ, এবার আমি শুভগকে আমাদের UI রিফ্রেশ সম্পর্কে কথা বলার জন্য আমন্ত্রণ জানাচ্ছি।
ধন্যবাদ, মার্টিন।
শেয়ার করা ইনফ্রাস্ট্রাকচার ব্যবহার করে, আমাদের লক্ষ্য ছিল আপনার কব্জিতে WhatsApp-কে সত্যিকারের ঘরের মতো অনুভব করানো।
সেই যাত্রার প্রথম অংশ ছিল Material 3-তে পরিবর্তন, তবে প্রথমে, আসুন জেনে নিই কেন।
একটি বৃত্তাকার ডিসপ্লেতে যেখানে প্রতিটি পিক্সেল গুরুত্বপূর্ণ, সেখানে Material 3 আমাদের সঠিক কম্পোনেন্ট এবং ডিজাইন ফাউন্ডেশন দিয়েছে যাতে আমরা কব্জির জন্য WhatsApp-কে আধুনিক করতে পারি।
আপনি যদি ভিজ্যুয়ালগুলি লক্ষ্য করেন, তাহলে আপাতদৃষ্টিতে এটি UI আপডেটের মতো মনে হতে পারে।
আমরা Material 2 থেকে 3-এ পরিবর্তন করছিলাম এবং এটি রাতারাতি করা সম্ভব ছিল না।
পুরনো ও নতুন কোডকে একটি সুইচের মাধ্যমে পাশাপাশি রাখতে হয়েছিল।
এর ফলে, Figma-র সাথে ভিজ্যুয়ালি ম্যাচ করা স্ক্রিনগুলি এখনও কিছু ভাঙা আচরণ দেখায়।
কয়েকটি উদাহরণ দিতে গেলে, ক্রাউন স্ক্রল করা, ফিরে যেতে ডানদিকে সোয়াইপ করা, সবচেয়ে সাধারণ কিছু জেসচার কাজ করেনি।
এইসব বলার উদ্দেশ্য হল, ডিজাইন সিস্টেম মাইগ্রেশন শুধু ভিজ্যুয়াল মাইগ্রেশন নয়, এটি আচরণগত মাইগ্রেশনও।
আচরণের কথা বলতে গেলে, আপনি যদি প্রান্তের কাছাকাছি সারিগুলি লক্ষ্য করেন, সেগুলি সংকীর্ণ এবং বক্ররেখায় আটকে থাকে।
এটি হল TransformingLazyColumn।
সারিগুলি পরিবর্তিত হয় যাতে বৃত্তাকার বেজেল সেগুলি কেটে না দেয়।
ডায়নামিক স্ক্রিন থাকলে, কাজ করা কঠিন হতে পারে।
নতুন মেসেজ আসে, সারি সরে যায় এবং বাবল বিভিন্ন আকারে আসে।
দেখা যাক, এটি ঠিক করার অর্থ কী।
আপনি যদি বাঁদিকে লক্ষ্য করেন, তাহলে দেখতে পাবেন যে বাবলগুলি খুব দ্রুত ছোট হয়ে যাচ্ছে।
আসলে, প্রতিটি মেসেজ ইন্টার্নালি একটি কার্ডের মধ্যে বাবল হিসেবে থাকে।
আমরা পেরেন্ট কার্ডকে ট্রান্সফর্ম প্রয়োগ করতে বলেছি, কিন্তু একই ট্রান্সফর্ম চাইল্ড বাবেলেও পাস করা হচ্ছে।
এর ফলে, চাইল্ড বাবলটি যতটা ট্রান্সফর্ম হওয়ার কথা ছিল তার দ্বিগুণ ট্রান্সফর্ম হচ্ছে।
সমাধানটি ছিল এটি নিশ্চিত করা যে আমরা একটি লেভেলে ট্রান্সফর্ম প্রয়োগ করি এবং সেটি পেরেন্ট কার্ডে ছিল।
আর এটিও একটি মূল নীতি।
আমরা নিশ্চিত করেছি যে একটি আইটেমের জন্য একটি ট্রান্সফর্মেশন করা হয়েছে।
এবং এখানে আরেকটি সমস্যা আছে: স্ক্রিনের ঠিক মাঝখানে ছোট এবং অস্পষ্ট বাবল দেখা যাচ্ছে, যেখানে সেগুলি সম্পূর্ণ আকারে থাকা উচিত।
স্ক্রিনে তাদের পজিশনের উপর নির্ভর করে সাইজ নির্ধারণ করা হয়।
আমাদের প্রাথমিক ভার্সনে, আমরা Compose-এর রিমেম্বার ফাংশন ব্যবহার করে সাইজ ফিটিং সেভ করেছি, কিন্তু এটি তার পুরনো পজিশনও সেভ করে।
এখন এর ফলে, যখন একটি নতুন মেসেজ আসে এবং এটি পুরনো বাবলগুলিকে উপরের দিকে সরিয়ে দেয়, তখন তাদের ট্রান্সফর্ম এখনও তাদের পুরনো পজিশনের উপর ভিত্তি করে হয়।
আর তার ফলে এই সমস্যা হয়।
এই সমস্যার সমাধান করতে, আমরা রিমেম্বার কল ব্যবহার করে সাইজ সেভ করা বন্ধ করে দিয়েছি এবং কল থেকে মুক্তি পেয়েছি।
আর এটিও একটি শিক্ষা।
কোনও সারি সরানো হলে, তার ট্রান্সফর্মেশনও তার সাথে সরাতে হবে।
এখানে আপনি কোডে দুটি সমাধানই দেখতে পাবেন।
// FIX #1: ONE OWNER
CompositionLocalProvider(
LocalSurfaceTransformation provides null
) {
M3Card(transformation = t) {
SharedChatEntry(...)
}
}
// FIX #2: CURRENT ITEM SCOPE
val t = SurfaceTransformation(spec)// BUG #1: DOUBLE MORPH
M3Card(transformation = t) {
CompositionLocalProvider(
LocalSurfaceTransformation provides t
) { SharedChatEntry(...) }
}
// BUG #2: STALE ITEM SCOPE
val t = remember(spec) {
SurfaceTransformation(spec)
}উপরে বাঁদিকে, আপনি দেখতে পাবেন যে ট্রান্সফর্ম দুটি লেভেলে প্রয়োগ করা হচ্ছে এবং উপরে ডানদিকে, এটি শুধুমাত্র একটি লেভেলে প্রয়োগ করা হচ্ছে, যা হল M3 কার্ড।
নিচে বাঁদিকে, ট্রান্সফর্ম ক্যাশে করার জন্য আমরা রিমেম্বার কল ব্যবহার করছি, তবে এটি পজিশনও ক্যাশে করে।
এবং নিচের ডানদিকে, আমরা প্রতিবার বাবলের লেটেস্ট পজিশন বিবেচনা করে ট্রান্সফর্ম গণনা করছি।
আপনি দেখতেই পাচ্ছেন, এর ফলে সব স্ক্রিনে একই ধরনের অভিজ্ঞতা পাওয়া যায়, যেখানে মূল কন্টেন্ট স্ক্রিনের মাঝখানে থাকে এবং সারিগুলি স্বাভাবিকভাবেই স্ক্রিনের কিনারার দিকে সরে যায়।
এরপর, বিজ্ঞপ্তি সম্পর্কে আলোচনা করা যাক।
এখন কল্পনা করুন যে আপনার স্মার্টওয়াচ ভাইব্রেট করছে, আপনি স্মার্টওয়াচ চেক করে একটি মেসেজ দেখতে পাচ্ছেন।
কিন্তু কিছুক্ষণ পরেই, আপনার কব্জি আবার, এবং আবার কেঁপে ওঠে।
আপনি একাধিক বিজ্ঞপ্তি পান এবং প্রতিবার চেক করার সময় একই জিনিস দেখতে পান।
বলতে বাধ্য হচ্ছি যে এটা সত্যিই বিরক্তিকর।
তাই এই সমস্যার সমাধান করতে, আমরা নিজেদের দুটি প্রশ্ন করেছি।
কোন ইভেন্ট ব্যবহারকারীকে বিজ্ঞপ্তি সম্পর্কে সতর্ক করবে?
এবং কোন ইভেন্ট বিজ্ঞপ্তি সাইলেন্টলি আপডেট করবে?
এই সমস্যাটি হচ্ছিল কারণ ব্যাকগ্রাউন্ডে প্রোফাইল ফটো আপডেট, মিডিয়া ডাউনলোড এবং রসিদ, যা বিজ্ঞপ্তিটি নিঃশব্দে আপডেট করার কথা ছিল, তার পরিবর্তে ব্যবহারকারীকে সতর্ক করছিল।
এখন, নতুন মেসেজ এলে, স্মার্টওয়াচ একবার ভাইব্রেট করে, অন্যদিকে সতর্কতা-যোগ্য নয় এমন ইভেন্ট বিজ্ঞপ্তিটি নিঃশব্দে রিফ্রেশ করে।
আর এটিও একটি শিক্ষা।
বিজ্ঞপ্তি যদি আপনার ব্যবহারকারীর অভিজ্ঞতার অন্যতম মূল অংশ হয়, তাহলে আমাদের নিশ্চিত করতে হবে যে প্রতিটি বিজ্ঞপ্তি যেন ব্যবহারকারীর মনোযোগ পাওয়ার যোগ্য হয়।
বিজ্ঞপ্তির বিষয়ে।
আগে, WhatsApp Wear OS বিজ্ঞপ্তির মাধ্যমে আপনি দুটি কাজ করতে পারতেন।
আপনি মেসেজটি পড়তে পারেন অথবা অ্যাপটি খুলতে পারেন।
আপনি যদি কোনও বিজ্ঞপ্তির সরাসরি উত্তর দিতে চাইতেন, তাহলে আগে তা করতে পারতেন না।
তাই আমরা RemoteInput API-এর সাথে Android-এর বিজ্ঞপ্তি অ্যাকশন ব্যবহার করে সেই ব্যবধান পূরণ করেছি।
ব্যবহারকারী এখন এটি টাইপ করতে, বলতে বা দ্রুত উত্তর দিতে ট্যাপ করতে পারবেন।
তবে মনে রাখবেন, 'পাঠান' বোতামে ক্লিক করলেই কাজ শেষ হয়ে যায় না।
কোনও মেসেজ পাঠানো হয়ে গেলে, আমরা চ্যাটটি 'পড়া হয়েছে' হিসেবে চিহ্নিত করি এবং বিজ্ঞপ্তি সরিয়ে দিই।
কোনও মেসেজ ডেলিভার করা না গেলে, আমরা একবার ট্যাপ করে আবার চেষ্টা করার সুবিধা দিই।
এবং এটি ডেভেলপ করার সময় এটিও একটি মূল শিক্ষা, তা হল একটি বিজ্ঞপ্তি অ্যাকশনকে অবশ্যই একটি সম্পূর্ণ এন্ড-টু-এন্ড ওয়ার্কফ্লো হিসাবে বিবেচনা করতে হবে।
এরপর, টাইল ও উইজেট।
আপনি দেখতে পাচ্ছেন যে, টাইল হল একটি ফুল স্ক্রিন, যেখানে আপনি ওয়াচফেস থেকে সোয়াইপ করে পৌঁছাতে পারবেন।
অন্যদিকে, উইজেট হল একাধিক ছোট কার্ড, যেখানে স্ক্রল করা যায় এমন একটি স্ক্রিনে অনেকগুলি ফিট হতে পারে।
WhatsApp টাইল ও উইজেট, দুটিই একই ডেটা দ্বারা পরিচালিত হয়।
আর এটিও একটি টুইস্ট।
আপনার মনে হতে পারে যে এই UI আমাদের অ্যাপের মাধ্যমে ড্র করা হচ্ছে, কিন্তু আসলে তা নয়।
আমরা শুধু UI ও ডেটার বিবরণ দিই এবং সিস্টেম সেটি ড্র করে।
এছাড়াও, কোনও আইকনে ট্যাপ করলে আপনার অ্যাপের কোড রান করতে পারে না।
আপনি শুধু এটিকে কোনও স্ক্রিন খুলতে বলতে পারেন।
আর এটাই আমাদের শিক্ষা।
সিস্টেম যদি আপনার UI ড্র করে, তাহলে এটি আপনার প্রোডাক্ট এবং টেস্টিং মেট্রিকের অংশ।
ফ্র্যাঙ্ক ও ইউরি উইজেট সম্পর্কে আরও বিস্তারিত আলোচনা করবেন।
আর এর মাধ্যমে আমি শেষ স্লাইডে চলে এসেছি, যেখানে আমি বলতে চাই যে, আসুন আমরা একটু পিছিয়ে গিয়ে নিজেদের প্রশ্ন করি, দিনের শেষে আমরা চাই যে আরও বেশি সংখ্যক ব্যবহারকারী আমাদের অ্যাপ ব্যবহার করুন।
এখন একজন ডেভেলপার হিসেবে কল্পনা করুন, আপনি একদিন আপনার ড্যাশবোর্ডে লগ-ইন করেছেন এবং আপনি একটি উল্লেখযোগ্য অ্যাক্টিভ ব্যবহারকারীর ক্লিফ দেখতে পাচ্ছেন।
প্রথমত, এটি খুবই উদ্বেগের বিষয়।
এবং তারপর আপনি এটি ডিবাগ করা শুরু করতে পারেন।
এবং আপনার মনে হতে পারে যে কোনও ক্র্যাশ হয়েছে, বা কোনও খারাপ রিলিজ চলছে।
কিন্তু আমাদের ক্ষেত্রে, এর কোনওটিই ঘটেনি।
দেখা গেছে যে WhatsApp অ্যাপের প্রতিটি ভার্সনের ৯০ দিনের বিল্ট-ইন মেয়াদ থাকে।
তাই, ভাবুন যে ব্যবহারকারীরা পুরনো বিল্ডে আটকে আছেন, যেখানে নতুন বিল্ড তাদের স্মার্টওয়াচে পৌঁছাতে সময় নিচ্ছে।
এর ফলে, ব্যবহারকারীরা অ্যাপ থেকে বেরিয়ে আসছেন না, কিন্তু তাদের অ্যাপ কাজ করা বন্ধ করে দিয়েছে।
তাই আমরা অ্যাপ আপডেট করার বিষয়টি প্রোডাক্টের অংশ হিসেবে অন্তর্ভুক্ত করেছি।
আপনি স্ক্রিনে দেখতে পাচ্ছেন যে অ্যাপের মেয়াদ শেষ হওয়ার ১৫ দিন আগে এটি স্ক্রিনে দেখা যায়।
তারপর ৭, ৫, ৩, ২ এবং ১ দিন বাকি থাকতে আবার এটি দেখা যায়।
ব্যবহারকারী এটি বাতিল করতে পারেন অথবা অ্যাপ আপডেট করার জন্য Play Store-এ যেতে পারেন।
এটি ব্যবহারকারীকে একাধিক রিমাইন্ডার দিয়ে সাহায্য করে যে তার অ্যাপের মেয়াদ শেষ হতে চলেছে এবং তার অ্যাপ আপডেট করার জন্য একাধিক সুযোগও দেয়।
আর এটাই আমাদের শেষ শিক্ষা।
১০০% আপডেট শিপ করার পরেও ডেভেলপার হিসেবে আমাদের কাজ শেষ হয় না।
ব্যবহারকারীরা আমাদের আপডেট পেলে তবেই এটি সম্পূর্ণ হয়।
আমরা একদম শেষে পৌঁছে গেছি।
আপনি যদি কথোপকথন চালিয়ে যেতে চান, তাহলে অফিসের সময় যেকোনও সময় হ্যালো বলতে পারেন।
এরপরে ফ্র্যাঙ্ক ও ইউরি Wear উইজেট সম্পর্কে কথা বলবেন।
ধন্যবাদ।
@Module
interface CallingModule {
@BindsOptionalOf
fun callingBackend(): CallingBackend
}
class Calls @Inject constructor(
val backend: Optional<CallingBackend>,
) {
fun start(): CallResult =
backend.getOrNull()?.start()
?: CallResult.NotAllowed
}@Module
interface CoreModule {
@Multibinds
fun handlers(): Set<MessageHandler>
}
@Module
interface ReactionsModule {
@Binds @IntoSet
fun reactions(impl: ReactionHandler):
MessageHandler
}
class MessageDispatcher @Inject constructor(
private val handlers: Set<MessageHandler>,
)<!-- remove unused Main activity node -->
<activity android:name="com.whatsapp.Main"
tools:node="remove" />
<!-- redirect any calls to a new Activity -->
<activity-alias
android:name="com.whatsapp.Main"
android:targetActivity=".MyNewActivity" />
<provider
android:name="…MigrationContentProvider"
tools:node="remove" /><!-- phone -->
<string name="…_disclosure">… Learn more</string>
<!-- wearable override, same key -->
<string name="…_disclosure">…Use your phone to learn more.</string>isRegistered.collect {
when (it) {
// watch posts its own
true -> bridging.setEnabled(false)
// phone bridges again
false -> bridging.setEnabled(true)
}
}
fun setEnabled(bridgingEnabled){
bridgingManager.setConfig(
BridgingConfig.Builder(context,
bridgingEnabled)
.build())
}clearActivityBackStack()
// enumerate our own, kill each
killOtherProcesses()
// no threads alive — now safe
deleteDataDirectories()
// show logged out confirmation
firePostCleanupIntent()// FIX #1: ONE OWNER
CompositionLocalProvider(
LocalSurfaceTransformation provides null
) {
M3Card(transformation = t) {
SharedChatEntry(...)
}
}
// FIX #2: CURRENT ITEM SCOPE
val t = SurfaceTransformation(spec)// BUG #1: DOUBLE MORPH
M3Card(transformation = t) {
CompositionLocalProvider(
LocalSurfaceTransformation provides t
) { SharedChatEntry(...) }
}
// BUG #2: STALE ITEM SCOPE
val t = remember(spec) {
SurfaceTransformation(spec)
}