Java Reactive Programming with Project Reactor | Sinhala Tutorial

Reactive Systems සහ Project Reactor: Non-Blocking Applications හදමු - Sinhala Guide
ආයුබෝවන් යාළුවනේ! අද අපි කතා කරන්න යන්නේ නූතන Software Engineering ලෝකයේ ගොඩක් වැදගත් මාතෘකාවක් ගැන. ඒ තමයි Reactive Programming සහ Project Reactor භාවිතයෙන් Non-Blocking Applications ගොඩනගන හැටි. ලොකු ඩිමාන්ඩ් එකක් තියෙන, ඉක්මනින් වැඩ කරන, පොඩ්ඩක්වත් හැංගි හැංගි ඉන්නේ නැති Systems හදන්න මේක අපිට ගොඩක් උදව් වෙනවා.
අද කාලේ Application එකකින් බලාපොරොත්තු වෙනවා Milliseconds ගානක් ඇතුළත රිස්පොන්ස් දෙන්න, ඒ වගේම එකවර Users ලා දහස් ගණනකට සර්විස් දෙන්න. සාම්ප්රදායික Block කරන ක්රමවේදයන් (Traditional Blocking Paradigms) මේ වගේ අවස්ථාවලදී අපහසුතාවන්ට පත් වෙනවා. ඉතින්, Reactive Programming කියන්නේ මේ අභියෝගයන්ට හොඳම විසඳුමක්. මේ ටියුටෝරියල් එකෙන් අපි Project Reactor කියන්නේ මොකක්ද, ඒකෙන් මොනවද කරන්න පුළුවන්, සහ ඒක Practical විදියට අපේ Java Applications වලට යොදාගන්නේ කොහොමද කියලා විස්තරාත්මකව බලමු.
මේ ගමන අන්තිමට, ඔබට Project Reactor ගැන පැහැදිලි අවබෝධයක් ලැබෙනවා වගේම, සරල Reactive Application එකක් හදන්න අවශ්ය දැනුම ලැබී තියෙයි. එහෙනම්, අපි පටන් ගමු!
Reactive Programming කියන්නේ මොකක්ද?
සරලවම කිව්වොත්, Reactive Programming කියන්නේ Asynchronous Data Streams එක්ක වැඩ කරන්න පුළුවන් Programming Paradigm එකක්. ඒ කියන්නේ, Data එකක් එනකොට (ඒක Data Stream එකක් විදියට), ඒකට ප්රතිචාර දක්වන (React කරන) විදියට Application එක හදන එක. මේකේදී සිද්ධ වෙන්නේ, අපි වැඩක් පටන් ගත්තම ඒක ඉවර වෙනකන් බලාගෙන ඉන්නේ නැතුව, ඊළඟ වැඩේට යන එක. වැඩේ ඉවර වුන ගමන් ඒක Notification එකක් විදියට අපිට දැනුම් දෙනවා.
Blocking vs. Non-Blocking
මේක තේරුම් ගන්න අපි පොඩි උදාහරණයක් බලමු:
- Blocking Operation: හිතන්න ඔයා Phone Call එකක් ගන්නවා කියලා. ඔයාගේ Call එක ඉවර වෙනකන් ඔයාට වෙන කිසිම වැඩක් කරන්න බැහැ. ඔයා Call එකට Block වෙලා ඉන්නේ. Programming වලදීත් මේ වගේම තමයි. Database එකකින් Data ගන්නකොට, External API එකකට Call කරනකොට, ඒ Operation එක ඉවර වෙනකන් Thread එක Wait කරනවා. ඒ කාලය තුළදී ඒ Thread එකට වෙන කිසිම වැඩක් කරන්න බැහැ. මේක Resources අපතේ යන ක්රමයක්.
- Non-Blocking Operation: දැන් හිතන්න ඔයා Internet එකෙන් File එකක් Download කරනවා කියලා. File එක Download වෙන ගමන් ඔයාට වෙන වැඩත් කරන්න පුළුවන්. File එක Download වුන ගමන් ඔයාට Notification එකක් එනවා. මේක තමයි Non-Blocking කියන්නේ. Programming වලදීත් මේ වගේම තමයි. Thread එකක් Database එකකට Request එකක් යැව්වම, ඒක ඒ Request එකට Block වෙන්නේ නැතුව, ඊළඟ Request එකට සර්විස් දෙන්න යනවා. කලින් Request එකට Data ආවම, ඒකට අදාළ Call-back එක Execute වෙනවා. මේකෙන් පුළුවන් සීමිත Threads ගානකින් වැඩි Request ප්රමාණයක් Handle කරන්න.
Reactive Programming වල ප්රධාන වාසි තියෙනවා:
- Responsiveness: Application එක හැමවෙලේම ඉක්මනින් ප්රතිචාර දක්වනවා. User Experience එක වැඩි වෙනවා.
- Resilience: එක් කොටසක ඇතිවන ගැටලුවක් මුළු System එකටම බලපාන්නේ නැහැ. Errors වුණත් Elegant විදියට Handle කරන්න පුළුවන්.
- Elasticity: වැඩිවන Load එකට අනුව System එක පහසුවෙන් Scale කරන්න පුළුවන්. අඩු Resources ප්රමාණයකින් වැඩි වැඩ ප්රමාණයක් කරන්න පුළුවන්.
- Message-Driven: Components අතර Asynchronous Message Passing එකකින් සන්නිවේදනය සිදුවෙනවා.
Project Reactor හඳුනාගනිමු
Java වල Reactive Applications හදන්න පුළුවන් Frameworks කීපයක් තියෙනවා. ඒ අතරින් බහුලවම භාවිතා වෙන එකක් තමයි Project Reactor. මේක Spring Framework එකත් එක්ක ගොඩක් හොඳට Integrate වෙනවා (විශේෂයෙන් Spring WebFlux).
Project Reactor කියන්නේ Reactive Streams Specification එක Implement කරන Library එකක්. Reactive Streams කියන්නේ Java වල Asynchronous Data Streams Non-blocking Backpressure එක්ක Handle කරන්න තියෙන Standard එකක්. ඒකෙන් Publishers, Subscribers, Subscriptions, සහ Processors කියන Core Interfaces ටික Defines කරනවා.
Core Components: Mono සහ Flux
Project Reactor වල හදවත වගේ වැඩ කරන ප්රධාන Classes දෙකක් තියෙනවා. ඒ තමයි Mono
සහ Flux
.
Flux
: මේකෙන් නියෝජනය කරන්නේ 0 සිට N (Zero to N) අයිතම නිකුත් කරන Asynchronous Sequence එකක්. ඒ කියන්නේ, මේකෙන් Data අයිතම කිහිපයක් එන්නත් පුළුවන්, නැතිව යන්නත් පුළුවන්. නැත්නම් Error එකක් එන්න පුළුවන්. Collection එකක් වගේ Data Stream එකක් (උදා: List of Users, Events Stream එකක්) බලාපොරොත්තු වෙන අවස්ථාවලදී Flux
එකක් භාවිතා කරනවා.
import reactor.core.publisher.Flux;
import java.util.Arrays;
import java.util.List;
public class FluxExample {
public static void main(String[] args) {
// A Flux that emits multiple values
Flux<String> namesFlux = Flux.just("Alice", "Bob", "Charlie", "David");
namesFlux.subscribe(System.out::println);
// A Flux from a List
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Flux<Integer> numbersFlux = Flux.fromIterable(numbers);
numbersFlux.map(n -> n * 2) // Operator: Transform each item
.filter(n -> n > 5) // Operator: Filter items
.subscribe(System.out::println,
error -> System.err.println("Error: " + error.getMessage()),
() -> System.out.println("Numbers Flux Completed!"));
}
}
Mono
: මේකෙන් නියෝජනය කරන්නේ 0ක් හෝ 1ක් (Zero or One) අයිතමයක් නිකුත් කරන Asynchronous Sequence එකක්. ඒ කියන්නේ, මේකෙන් Data එකක් එන්නත් පුළුවන්, නැතිව යන්නත් පුළුවන්. නැත්නම් Error එකක් එන්න පුළුවන්. Single Result එකක් (උදා: User Object එකක්, ඒ Request එකේ Response එක) බලාපොරොත්තු වෙන අවස්ථාවලදී Mono
එකක් භාවිතා කරනවා.
import reactor.core.publisher.Mono;
public class MonoExample {
public static void main(String[] args) {
// A Mono that emits a single value
Mono<String> greetingMono = Mono.just("Hello Reactor!");
greetingMono.subscribe(System.out::println);
// A Mono that emits no value (empty)
Mono<String> emptyMono = Mono.empty();
emptyMono.subscribe(System.out::println,
error -> System.err.println("Error: " + error.getMessage()),
() -> System.out.println("Empty Mono completed!"));
// A Mono that emits an error
Mono<Integer> errorMono = Mono.error(new RuntimeException("Something went wrong!"));
errorMono.subscribe(System.out::println,
error -> System.err.println("Error from errorMono: " + error.getMessage()));
}
}
Mono
සහ Flux
දෙකම Publisher
Interface එක Implement කරනවා. ඒ කියන්නේ, මේවා Data Streams නිපදවන (Publish කරන) දේවල්. මේ Data Streams වලට Subscribe වෙනකන් (ඒ කියන්නේ .subscribe()
Method එක Call කරනකන්) කිසිම දෙයක් සිද්ධ වෙන්නේ නැහැ. මේකට කියන්නේ Cold Publishers කියලා.
Operators: Data Transformations සහ Manipulations
Project Reactor වල තියෙන ප්රබලම විශේෂාංගයක් තමයි Operators. මේවා Mono
සහ Flux
මත Call කරන්න පුළුවන් Methods. මේවා Data Stream එක වෙනස් කරන්න, Filter කරන්න, Merge කරන්න, නැත්නම් වෙනත් දේවල් කරන්න උදව් වෙනවා. අපි උඩ FluxExample
එකේදී .map()
සහ .filter()
භාවිතා කළා මතකද? ඒ වගේ තව දහස් ගණන් Operators තියෙනවා.
වැඩිපුරම භාවිතා වන Operator කීපයක්:
map(Function)
: එක් එක් අයිතමය වෙනත් ආකාරයකට පරිවර්තනය කරයි.flatMap(Function)
: එක් එක් අයිතමය වෙනත් Mono/Flux එකකට පරිවර්තනය කරයි, පසුව එම Publishers Combine කරයි. Non-blocking ලෙස Chained Operations කරන්න මෙය ඉතා වැදගත්.filter(Predicate)
: යම් Condition එකකට අදාළ වන අයිතම පමණක් තෝරා ගනී.doOnNext(Consumer)
: එක් එක් අයිතමය නිකුත් කරන විට Side Effect එකක් (උදා: Logging) සිදු කරයි.onErrorResume(Function)
: Error එකක් ඇති වූ විට වෙනත් Publisher එකකින් Resume කිරීමට ඉඩ සලසයි.zip(Publisher)
: Publishers කිහිපයකින් අයිතම Combine කර Tuple එකක් ලෙස නිකුත් කරයි.delayElements(Duration)
: එක් එක් අයිතමය නිකුත් කිරීමට පෙර යම් කාලයක් ප්රමාද කරයි.
Mono සහ Flux භාවිතය: Practical Examples
අපි දැන් තව පොඩ්ඩක් සංකීර්ණ උදාහරණයක් බලමු. User Profile Details Load කරන Service එකක් හදමු.
import reactor.core.publisher.Mono;
import java.time.Duration;
public class UserProfileService {
// Simulates fetching user details from a database (non-blocking)
public Mono<String> getUserDetails(String userId) {
// Simulate network/DB delay
return Mono.just("Details for User: " + userId)
.delayElement(Duration.ofSeconds(2))
.doOnNext(data -> System.out.println("Fetched data for " + userId + ": " + data));
}
// Simulates fetching user preferences from another service (non-blocking)
public Mono<String> getUserPreferences(String userId) {
return Mono.just("Preferences for User: " + userId)
.delayElement(Duration.ofSeconds(1))
.doOnNext(data -> System.out.println("Fetched preferences for " + userId + ": " + data));
}
// Combine user details and preferences
public Mono<String> getFullUserProfile(String userId) {
Mono<String> detailsMono = getUserDetails(userId);
Mono<String> preferencesMono = getUserPreferences(userId);
return Mono.zip(detailsMono, preferencesMono,
(details, preferences) -> details + ", " + preferences)
.doOnSuccess(data -> System.out.println("Combined full profile for " + userId + ": " + data));
}
public static void main(String[] args) throws InterruptedException {
UserProfileService service = new UserProfileService();
long startTime = System.currentTimeMillis();
System.out.println("Starting to fetch user profiles...");
// Fetch a single user profile
service.getFullUserProfile("user123")
.subscribe(profile -> System.out.println("User Profile: " + profile),
error -> System.err.println("Error fetching profile: " + error.getMessage()),
() -> System.out.println("User123 profile fetch completed."));
// Fetch another user profile concurrently
service.getFullUserProfile("user456")
.subscribe(profile -> System.out.println("User Profile: " + profile),
error -> System.err.println("Error fetching profile: " + error.getMessage()),
() -> System.out.println("User456 profile fetch completed."));
// Keep the main thread alive to see asynchronous operations complete
Thread.sleep(5000);
long endTime = System.currentTimeMillis();
System.out.println("Total execution time: " + (endTime - startTime) + " ms");
}
}
මේ උදාහරණයේදී, getUserDetails
සහ getUserPreferences
කියන Methods දෙකම Mono
එකක් Return කරනවා. ඒ කියන්නේ ඒ Operations දෙකම Non-blocking. zip
Operator එක භාවිතා කරලා අපි ඒ Mono
දෙකේ Results එකට Combine කරනවා. මේකේදී ඒ Operations දෙකම එකිනෙකට ස්වාධීනව (independently) එකම වෙලාවේ Execute වෙන නිසා, මුළු Operation එකටම ගතවන කාලය අඩු වෙනවා (blocking call එකකදී වගේ අනුපිළිවෙලින් වෙන්නේ නැහැ).
Non-Blocking API එකක් ගොඩනගමු (Spring WebFlux සමග)
Reactive Programming වල බලය හොඳින්ම පෙනෙන්නේ Microservices සහ Web Applications හදනකොට. Spring Framework එකේ Spring WebFlux කියන Module එක Reactive Programming වලටම විශේෂයෙන් හදපු Framework එකක්. මේක Traditional Spring MVC වගේ Blocking Model එකට වැඩ කරන්නේ නැහැ. Non-blocking Reactive Streams Model එකට අනුව තමයි මේක ගොඩනැගිලා තියෙන්නේ.
අපි දැන් Spring WebFlux භාවිතයෙන් සරල REST API එකක් හදමු.
Project Setup
මුලින්ම, Spring Initializr (start.spring.io) එකට ගිහින් පහත Dependencies තෝරලා Project එක Generate කරගන්න:
- Spring Reactive Web (WebFlux)
- Spring Data R2DBC (If you need Reactive Database access)
- Lombok (Optional, for boilerplate reduction)
Code Example: Reactive User API
අපි Users ලා Manage කරන්න සරල API එකක් හදමු.
User.java (Model)
package com.example.reactiveapi.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String id;
private String name;
private int age;
}
UserService.java (Service Layer)
මේ Service එකේදී අපි Simulate කරන්නේ Database එකක් හෝ External Service එකකින් Data ගන්නවා වගේ. අපි delayElement
එකක් භාවිතා කරන්නේ Non-blocking ස්වභාවය පෙන්වන්නයි.
package com.example.reactiveapi.service;
import com.example.reactiveapi.model.User;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
private final Map<String, User> users = new HashMap<>();
public UserService() {
users.put("1", new User("1", "Alice", 30));
users.put("2", new User("2", "Bob", 24));
users.put("3", new User("3", "Charlie", 35));
}
// Returns a Flux of all users, with a simulated delay for each user
public Flux<User> getAllUsers() {
return Flux.fromIterable(users.values())
.delayElements(Duration.ofMillis(100)) // Simulate streaming data with delay
.doOnNext(user -> System.out.println("Fetching user: " + user.getName()));
}
// Returns a Mono for a single user by ID, with a simulated delay
public Mono<User> getUserById(String id) {
return Mono.justOrEmpty(users.get(id))
.delayElement(Duration.ofMillis(500)) // Simulate network/DB latency
.switchIfEmpty(Mono.error(new RuntimeException("User not found with ID: " + id)))
.doOnNext(user -> System.out.println("Fetching user by ID: " + user.getName()));
}
// Saves a new user and returns a Mono of the saved user
public Mono<User> saveUser(User user) {
String newId = String.valueOf(users.size() + 1);
user.setId(newId);
users.put(newId, user);
return Mono.just(user)
.delayElement(Duration.ofMillis(300))
.doOnNext(u -> System.out.println("User saved: " + u.getName()));
}
// Deletes a user by ID and returns a Mono<Void> (no content)
public Mono<Void> deleteUser(String id) {
return Mono.fromRunnable(() -> users.remove(id))
.delayElement(Duration.ofMillis(200))
.then(); // Emits a complete signal when the Runnable finishes
}
}
UserController.java (Controller Layer)
Controller එකේදී අපි Flux
සහ Mono
Return කරනවා. Spring WebFlux මගින් මේවා HTTP Responses වලට Automatically Convert කරනවා.
package com.example.reactiveapi.controller;
import com.example.reactiveapi.model.User;
import com.example.reactiveapi.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) // For streaming response
public Flux<User> getAllUsers() {
return userService.getAllUsers();
}
@GetMapping("/{id}")
public Mono<User> getUserById(@PathVariable String id) {
return userService.getUserById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<User> createUser(@RequestBody User user) {
return userService.saveUser(user);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> deleteUser(@PathVariable String id) {
return userService.deleteUser(id);
}
// Global error handling for UserNotFoundException
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Mono<String> handleNotFoundException(RuntimeException ex) {
return Mono.just(ex.getMessage());
}
}
මේ API එක Run කරලා බලන්න:
GET /users
: Streaming Response එකක් විදියට User Data ටික ටික එනවා. Browser එකෙන් බලනකොට එකපාරටම JSON Array එකක් විදියට පෙනුනත්, Backend එකේදී Data Stream වෙනවා.GET /users/1
: Single User Details එකක් එනවා. (500ms delay එකක් එක්ක)POST /users
: New User කෙනෙක් Add කරන්න පුළුවන්.DELETE /users/1
: User කෙනෙක් Delete කරන්න පුළුවන්.
මේක Blocking API එකක් වගේම Call කරන්න පුළුවන් වුනත්, Backend එකේදී සම්පූර්ණයෙන්ම Non-blocking විදියට වැඩ කරන්නේ. මේක තමයි Reactive Programming වල තියෙන සුන්දරත්වය.
වැදගත් කරුණු සහ හොඳ පුරුදු (Best Practices)
Reactive Programming වලට අලුත් අයට මුලදී ටිකක් සංකීර්ණ වෙන්න පුළුවන්. ඒත් මේ දේවල් මතක තියාගත්තොත් වැඩේ පහසු කරගන්න පුළුවන්.
1. Backpressure
Publisher කෙනෙක් Subscriber කෙනෙක්ට Handle කරන්න බැරි තරම් වේගයකින් Data යවනකොට සිද්ධ වෙන ගැටලුව තමයි Backpressure කියන්නේ. Reactive Streams Specification එක මේ ගැටලුවට විසඳුම් සපයනවා. Subscriber ට පුළුවන් Publisher ට කියන්න "මට මෙච්චර Data විතරයි ඕන" කියලා. Project Reactor මගින් මේ Backpressure යාන්ත්රණය Automatically Manage කරනවා. හැබැයි අපි Operators භාවිතා කරනකොට මේ ගැන අවබෝධයක් තියෙන එක වැදගත්.
2. Context Management
Reactive Streams වලට Context එකක් (ThreadLocal වගේ) කෙලින්ම ගෙනියන්න බැහැ. Reactor වලට තමන්ගේම Context
API එකක් තියෙනවා. මේකෙන් පුළුවන් Subscriber සිට Operator Chain එක දක්වා තොරතුරු යවන්න. Security Details, Trace IDs වගේ දේවල් යවන්න මේක ප්රයෝජනවත් වෙනවා.
3. Error Handling
Reactive Streams වල Error handling ඉතා වැදගත්. Error එකක් ආවොත් Stream එක Terminate වෙනවා. ඒ නිසා අපි onErrorReturn()
, onErrorResume()
, doOnError()
වගේ Operators භාවිතා කරලා Errors Elegant විදියට Handle කරන්න ඕනේ.
4. Testing Reactive Code
Reactive code Testing කිරීම සාම්ප්රදායික Unit Testing වලට වඩා වෙනස්. Reactor-Test Library එකේ තියෙන StepVerifier
කියන Class එක මේකට ගොඩක් ප්රයෝජනවත්. මේකෙන් Asynchronous Sequence එකක් Expected Events වලට අනුව හැසිරෙනවාද කියලා Verify කරන්න පුළුවන්.
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.time.Duration;
public class MyReactiveServiceTest {
@Test
void testFluxSequence() {
Flux<String> names = Flux.just("Alice", "Bob", "Charlie");
StepVerifier.create(names)
.expectNext("Alice")
.expectNext("Bob")
.expectNext("Charlie")
.verifyComplete();
}
@Test
void testFluxErrorHandling() {
Flux<String> errorFlux = Flux.just("data1", "data2")
.concatWith(Flux.error(new RuntimeException("Test Error")));
StepVerifier.create(errorFlux)
.expectNext("data1")
.expectNext("data2")
.expectErrorMessage("Test Error")
.verify();
}
@Test
void testMonoWithDelay() {
Mono<String> data = Mono.just("Hello").delayElement(Duration.ofMillis(100));
StepVerifier.withVirtualTime(() -> data)
.thenAwait(Duration.ofMillis(100))
.expectNext("Hello")
.verifyComplete();
}
}
5. Threading Model
Project Reactor වල Threading Model එක ගොඩක් Flexible. Operators බොහෝ විට Non-blocking වුවත්, සමහර Operators (උදා: publishOn()
, subscribeOn()
) භාවිතයෙන් Execution Context එක වෙනස් කරන්න පුළුවන්. නමුත් සාමාන්යයෙන් Reactive Programming වලදී අපිට Thread Management ගැන වැඩිය හිතන්න අවශ්ය වෙන්නේ නැහැ. Framework එක ඒක බලාගන්නවා.
අවසාන වචනය (Conclusion)
ඉතින් යාළුවනේ, මේ ටියුටෝරියල් එකෙන් අපි Reactive Programming කියන්නේ මොකක්ද, ඒකේ වාසි මොනවද, සහ Project Reactor භාවිතයෙන් Java වල Non-blocking Applications හදන්නේ කොහොමද කියලා විස්තරාත්මකව ඉගෙන ගත්තා. Mono
සහ Flux
කියන Core Building Blocks ගැන වගේම, Operators භාවිතයෙන් Data Streams Manipulate කරන හැටිත්, Spring WebFlux සමග Reactive API එකක් හදන හැටිත් අපි බැලුවා.
Reactive Systems කියන්නේ Future එක. වැඩිවන Load එකට අනුව පහසුවෙන් Scale කරන්න පුළුවන්, ඉක්මනින් ප්රතිචාර දක්වන Applications හදන්න මේ දැනුම ඔබට ගොඩක් ප්රයෝජනවත් වෙයි. මුලදී ටිකක් අමාරු වුනත්, පුහුණුවෙන් මේක පහසු වෙනවා. දැන් ඔබත් මේ Concepts අරගෙන ඔබගේ Project වලට Implement කරන්න උත්සාහ කරන්න.
ඔබගේ අදහස්, ප්රශ්න හෝ මේ ගැන ඔබගේ අත්දැකීම් පහලින් Comment කරන්න. තව මොන වගේ මාතෘකා ගැනද දැනගන්න කැමති කියලත් කියන්න. මේ වගේම තවත් Tutorials අරගෙන අපි ඉක්මනින්ම එනවා. එතකන් හැමෝටම ජය!