Compose UI আর্কিটেকচার

Compose-এ UI অপরিবর্তনীয়—এটি একবার আঁকা হয়ে গেলে, আপডেট করার কোনও উপায় নেই। আপনি যা কন্ট্রোল করতে পারবেন তা হল আপনার UI-এর অবস্থা। UI-এর স্ট্যাটাস যতবার পরিবর্তিত হয়, Compose UI ট্রির পরিবর্তিত অংশগুলি ততবার আবার তৈরি করে। Composables, State গ্রহণ করতে এবং ইভেন্ট এক্সপোজ করতে পারে—যেমন, একটি TextField, ভ্যালু গ্রহণ করে এবং কলব্যাক onValueChange এক্সপোজ করে যা কলব্যাক হ্যান্ডলারকে ভ্যালু পরিবর্তন করার অনুরোধ করে।

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

যেহেতু কম্পোজেবল স্টেট গ্রহণ করে এবং ইভেন্ট প্রকাশ করে, তাই ইউনিডিরেকশনাল ডেটা ফ্লো প্যাটার্ন Jetpack Compose-এর সাথে ভালভাবে মানানসই। Compose-এ কীভাবে ইউনিডিরেকশনাল ডেটা ফ্লো প্যাটার্ন, ইভেন্ট ও স্টেট হোল্ডার প্রয়োগ করতে হয় এবং Compose-এ কীভাবে ViewModel-এর সাথে কাজ করতে হয়, এই গাইডে সেই বিষয়ে ফোকাস করা হয়েছে।

একমুখী ডেটা ফ্লো

ইউনিডিরেকশনাল ডেটা ফ্লো (UDF) হল এমন একটি ডিজাইন প্যাটার্ন যেখানে স্টেট নিচের দিকে এবং ইভেন্ট উপরের দিকে ফ্লো করে। ইউনিডিরেকশনাল ডেটা ফ্লো অনুসরণ করে, আপনি কম্পোজেবলকে ডিকাপল করতে পারবেন যা UI-তে স্টেট দেখায়, আপনার অ্যাপের সেইসব অংশ থেকে যা স্টেট স্টোর ও পরিবর্তন করে।

ইউনিডিরেকশনাল ডেটা ফ্লো ব্যবহার করে এমন অ্যাপের জন্য UI আপডেট লুপটি এইরকম দেখায়:

  1. ইভেন্ট: UI-এর অংশ একটি ইভেন্ট তৈরি করে এবং এটিকে উপরের দিকে পাস করে, যেমন একটি ViewModel-এ পাস করা বোতাম ক্লিক হ্যান্ডেল করতে; অথবা একটি ইভেন্ট থেকে পাস করা হয় আপনার অ্যাপের অন্যান্য লেয়ার, যেমন ব্যবহারকারীর সেশন আছে তা নির্দেশ করে মেয়াদ শেষ।
  2. স্ট্যাটাস আপডেট করা: কোনও ইভেন্ট হ্যান্ডলার স্ট্যাটাস পরিবর্তন করতে পারে।
  3. ডিসপ্লে স্টেট: স্টেট হোল্ডার স্টেট পাস করে এবং UI সেটি দেখায়।
ইভেন্ট UI থেকে স্টেট হোল্ডারে যায় এবং স্টেট, স্টেট হোল্ডার থেকে UI-তে যায়।
ছবি ১. একমুখী ডেটা ফ্লো।

Jetpack Compose ব্যবহার করার সময় এই প্যাটার্ন অনুসরণ করলে অনেক সুবিধা পাওয়া যায়:

  • টেস্ট করার সুবিধা: UI থেকে স্টেট ডিকাপল করলে, দুটিকেই আলাদাভাবে টেস্ট করা সহজ হয়।
  • স্টেট এনক্যাপসুলেশন: যেহেতু স্টেট শুধুমাত্র একটি জায়গায় আপডেট করা যায় এবং কম্পোজেবলের স্টেটের জন্য শুধুমাত্র একটি সোর্স অফ ট্রুথ থাকে, তাই অসামঞ্জস্যপূর্ণ স্টেটের কারণে আপনার বাগ তৈরি করার সম্ভাবনা কম।
  • UI-এর ধারাবাহিকতা: StateFlow বা LiveData-এর মতো পর্যবেক্ষণযোগ্য স্টেট হোল্ডার ব্যবহার করে UI-তে সব স্টেট আপডেট অবিলম্বে দেখানো হয়।

Jetpack Compose-এ ইউনিডিরেকশনাল ডেটা ফ্লো

কম্পোজেবল স্টেট ও ইভেন্টের উপর ভিত্তি করে কাজ করে। যেমন, TextField-এর value প্যারামিটার আপডেট করা হলে তবেই সেটি আপডেট হয় এবং এটি onValueChange কলব্যাক প্রকাশ করে—এটি এমন একটি ইভেন্ট যা ভ্যালু পরিবর্তন করে নতুন ভ্যালু সেট করার অনুরোধ করে। Compose State অবজেক্টকে ভ্যালু হোল্ডার হিসেবে সংজ্ঞায়িত করে এবং স্টেট ভ্যালুতে পরিবর্তন হলে রিকম্পোজিশন ট্রিগার করে। কতক্ষণ ভ্যালু মনে রাখতে হবে তার উপর নির্ভর করে আপনি remember { mutableStateOf(value) } বা rememberSaveable { mutableStateOf(value) }-এ স্টেট ধরে রাখতে পারবেন।

TextField কম্পোজেবলের ভ্যালুর ধরন হল String, তাই এটি যেকোনও জায়গা থেকে আসতে পারে—হার্ডকোড করা ভ্যালু থেকে, ViewModel থেকে অথবা পেরেন্ট কম্পোজেবল থেকে পাস করা। আপনাকে এটি State অবজেক্টে রাখতে হবে না, তবে onValueChange কল করা হলে আপনাকে ভ্যালু আপডেট করতে হবে।

  • mutableStateOf(value) একটি MutableState তৈরি করে, যা Compose-এ একটি পর্যবেক্ষণযোগ্য টাইপ। এর ভ্যালুতে কোনও পরিবর্তন হলে, সেই ভ্যালু রিড করে এমন যেকোনও কম্পোজেবল ফাংশনের ভ্যালু শিডিউল রিকম্পোজিশন হয়।
  • remember কম্পোজিশনে অবজেক্ট স্টোর করে এবং কম্পোজিশন থেকে remember-কে কল করা কম্পোজেবল সরিয়ে দেওয়া হলে অবজেক্ট ভুলে যায়।
  • rememberSaveable কনফিগারেশন পরিবর্তন জুড়ে স্টেট ধরে রাখে, এটি Bundle-এ সেভ করে।

কম্পোজেবল প্যারামিটার নির্ধারণ করুন

কম্পোজেবলের স্টেট প্যারামিটার সংজ্ঞায়িত করার সময়, নিম্নলিখিত প্রশ্নগুলি মনে রাখবেন:

  • কম্পোজেবল কতটা পুনর্ব্যবহারযোগ্য বা নমনীয়?
  • স্টেট প্যারামিটার কীভাবে এই কম্পোজেবলের পারফর্ম্যান্সকে প্রভাবিত করে?

ডিকাপলিং ও রিইউজ প্রোমোট করতে, প্রতিটি কম্পোজেবলে যতটা সম্ভব কম তথ্য থাকতে হবে। যেমন, কোনও খবর সম্পর্কিত নিবন্ধের হেডার ধরে রাখার জন্য কম্পোজেবল তৈরি করার সময়, পুরো খবর সম্পর্কিত নিবন্ধের পরিবর্তে শুধুমাত্র যে তথ্যটি দেখানো প্রয়োজন সেটি পাস করাকে অগ্রাধিকার দিন:

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

কখনও কখনও, স্বতন্ত্র প্যারামিটার ব্যবহার করলে পারফর্ম্যান্সও উন্নত হয়—যেমন, News-এ শুধু title ও subtitle-এর চেয়ে বেশি তথ্য থাকলে, যখনই News-এর নতুন ইনস্ট্যান্স Header(news)-এ পাস করা হয়, কম্পোজেবলটি আবার কম্পোজ করবে, এমনকি title ও subtitle পরিবর্তন না হলেও।

আপনি কতগুলি প্যারামিটার পাস করছেন তা ভালভাবে বিবেচনা করুন। অনেক বেশি প্যারামিটার সহ ফাংশন থাকলে, ফাংশনের এরগোনমিক্স কমে যায়, তাই এই ক্ষেত্রে একটি ক্লাসে সেগুলিকে গ্রুপ করা ভালো।

Compose-এ ইভেন্ট

আপনার অ্যাপে প্রতিটি ইনপুট একটি ইভেন্ট হিসেবে দেখানো উচিত: ট্যাপ, টেক্সট পরিবর্তন এমনকি টাইমার বা অন্যান্য আপডেট। এইসব ইভেন্ট আপনার UI-এর অবস্থা পরিবর্তন করে বলে, ViewModel-কে সেগুলি ম্যানেজ করতে হবে এবং UI-এর অবস্থা আপডেট করতে হবে।

UI লেয়ারের কখনই ইভেন্ট হ্যান্ডলারের বাইরে স্ট্যাটাস পরিবর্তন করা উচিত নয় কারণ এটি আপনার অ্যাপ্লিকেশনে অসঙ্গতি এবং বাগ তৈরি করতে পারে।

স্টেট ও ইভেন্ট হ্যান্ডলার ল্যাম্বডার জন্য অপরিবর্তনীয় ভ্যালু পাস করাকে অগ্রাধিকার দিন। এই পদ্ধতির নিম্নলিখিত সুবিধাগুলি রয়েছে:

  • আপনি পুনর্ব্যবহারযোগ্যতা উন্নত করেন।
  • আপনি যাচাই করেন যে আপনার UI সরাসরি স্ট্যাটাসের ভ্যালু পরিবর্তন করে না।
  • আপনি কনকারেন্সি সংক্রান্ত সমস্যা এড়িয়ে যান কারণ আপনি নিশ্চিত করেন যে অন্য থ্রেড থেকে স্টেট মিউটেট করা হয়নি।
  • আপনি প্রায়ই কোডের জটিলতা কমান।

যেমন, প্যারামিটার হিসেবে String ও ল্যাম্বডা গ্রহণ করে এমন কম্পোজেবলকে অনেক প্রসঙ্গ থেকে কল করা যেতে পারে এবং এটি অত্যন্ত পুনর্ব্যবহারযোগ্য। ধরুন, আপনার অ্যাপের উপরের অ্যাপ বারে সবসময় টেক্সট ও একটি ফিরে যাওয়ার বোতাম দেখানো হয়। আপনি আরও জেনেরিক MyAppTopAppBar কম্পোজেবল সংজ্ঞায়িত করতে পারেন যা টেক্সট এবং ব্যাক বোতাম হ্যান্ডলারকে প্যারামিটার হিসেবে গ্রহণ করে:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModels, স্টেট ও ইভেন্ট: একটি উদাহরণ

ViewModel এবং mutableStateOf ব্যবহার করে, আপনি নিজের অ্যাপে ইউনিডিরেকশনাল ডেটা ফ্লোও ইন্ট্রোডিউস করতে পারবেন, যদি নিম্নলিখিতগুলির মধ্যে কোনও একটি সত্য হয়:

  • আপনার UI-এর স্টেট StateFlow বা LiveData-এর মতো পর্যবেক্ষণযোগ্য স্টেট হোল্ডার ব্যবহার করে দেখানো হয়।
  • ViewModel আপনার অ্যাপের UI বা অন্যান্য লেয়ার থেকে আসা ইভেন্ট ম্যানেজ করে এবং ইভেন্টের উপর ভিত্তি করে স্টেট হোল্ডার আপডেট করে।

যেমন, সাইন-ইন স্ক্রিন প্রয়োগ করার সময়, সাইন-ইন করুন বোতামে ট্যাপ করলে আপনার অ্যাপে প্রগ্রেস স্পিনার ও নেটওয়ার্ক কল দেখানো উচিত। লগ-ইন সফল হলে, আপনার অ্যাপ অন্য স্ক্রিনে নেভিগেট করে; কোনও সমস্যা হলে অ্যাপটি একটি স্ন্যাকবার দেখায়। আপনি কীভাবে স্ক্রিন স্টেট ও ইভেন্ট মডেল করবেন তা এখানে দেওয়া হল:

স্ক্রিনের চারটি অবস্থা আছে:

  • সাইন-আউট করা: যখন ব্যবহারকারী এখনও সাইন-ইন করেননি।
  • প্রসেস চলছে: যখন আপনার অ্যাপ নেটওয়ার্ক কল পারফর্ম করার মাধ্যমে ব্যবহারকারীকে সাইন-ইন করানোর চেষ্টা করছে।
  • সমস্যা: সাইন-ইন করার সময় কোনও সমস্যা হলে।
  • সাইন-ইন করা আছে: ব্যবহারকারী সাইন-ইন করে থাকলে।

আপনি এই স্টেটগুলিকে একটি সিল করা ক্লাস হিসাবে মডেল করতে পারেন। ViewModel State হিসেবে স্টেট প্রকাশ করে, প্রাথমিক স্টেট সেট করে এবং প্রয়োজন অনুযায়ী স্টেট আপডেট করে। ViewModel একটি onSignIn() পদ্ধতি প্রকাশ করে সাইন-ইন ইভেন্টও ম্যানেজ করে।

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

mutableStateOf API ছাড়াও, Compose লিসেনার হিসেবে রেজিস্টার করতে এবং ভ্যালুকে স্টেট হিসেবে দেখাতে LiveData, Flow ও Observable-এর জন্য এক্সটেনশন প্রদান করে।

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

আরও জানুন

Jetpack Compose-এ আর্কিটেকচার সম্পর্কে আরও জানতে, নিম্নলিখিত রিসোর্সগুলি দেখুন:

নমুনা