"ไม่ต้องมีเวทมนตร์ ไม่ต้องไปหาแม่มด แค่คุณทำสิ่งที่โลกระลึกถึงตลอดกาล แค่นั้นคุณก็เป็นอมตะแล้ว"
[Geek อีกละ] ทำความรู้จักกับ Xamarin เครื่องมือพัฒนาที่มาพร้อม Concept "แชร์โค้ดระหว่างแพลตฟอร์ม"
13 Oct 2014 01:23   [32761 views]

Geek ต่ออีก Blog ละกันนะ

ตอนนี้ก็ราวๆ 5-6 ปีละที่เครื่องมือพัฒนาแอพฯบนมือถือถูกพัฒนาขึ้นเรื่อยๆ เทคนิคที่ใช้ก็เรียกว่าแพรวพราว มีให้เลือกกันพรึบพรั่บ ถ้าแบ่งเป็นประเภทคร่าวๆก็มีดังต่อไปนี้

1) Native - ภาษาที่ทางผู้พัฒนาแพลตฟอร์มเป็นคนจัดมาให้ (Android - Java, iOS - ObjC/Swift, Windows Phone - C#)

2) Hybrid - HTML5 + CSS + Javascript มีการเชื่อมต่อกับ Runtime เพื่อเรียกคำสั่งบางอย่างที่ HTML5 ทำไม่ได้ พวก PhoneGap, Kendo จะอยู่ในจำพวกนี้

3) ภาษาอื่นที่คอมไพล์เป็น Native - เขียนด้วยภาษาที่ทำขึ้นมาเอง แต่สุดท้ายคอมไพล์ออกมาเป็น Native ซึ่งประเภทนี้ก็แบ่งออกเป็นสองแบบอีกคือ

- ประเภทที่เขียนทีเดียวรันได้ทุกแพลตฟอร์ม (Write Once Run Anywhere) เช่น MarmaladeUnity3D, libgdx ฯลฯ

- พวกที่แชร์โค้ดกันระหว่างแพลตฟอร์มได้ส่วนหนึ่ง แต่อีกส่วนหนึ่งต้องเขียนแยกกันไปตามแต่ละ Platform (Code Sharing) เช่น Xamarin

และที่เราจะมาพูดถึงวันนี้คือของที่มาพร้อมกับแนวคิดแปลกใหม่อย่าง Xamarin ครับ

แนวคิดแปลกใหม่ Code Sharing ระหว่างแพลตฟอร์ม แต่ไม่ Write Once Run Anywhere

จุดแข็งของ Native คือประสิทธิภาพดี แต่แลกมากับข้อเสียที่ว่าเขียนยากพอควร (ยกเว้น Swift ที่ตอนนี้เขียนง่ายสาดดด) และข้อเสียใหญ่อีกข้อคือ เขียนบน Platform ไหน ก็รันได้แค่บน Platform นั้น ถ้าจะไปรันบนแพลตฟอร์มอื่นก็ต้องเขียนใหม่เท่านั้น

จึงมีคนคิดค้นเทคโนโลยีพัฒนาแอพฯ Cross Platform แบบ Write Once Run Anywhere หรือเขียนทีเดียวรันได้หมดทุกแพลตฟอร์ม หลักๆมีอยู่สองแนวคิดคือ

1) Hybrid - ใช้ HTML5 พัฒนาแอพฯขึ้นมา ข้อเสียหนักๆอยู่ที่เรื่องของประสิทธิภาพ โดยเฉพาะอย่างยิ่งมือถือที่ Mass อย่างแอนดรอยด์ราคาถูก (พูดง่ายๆคือมีปัญหากับเครื่องส่วนมาก) และการทำ UI ให้ดูสอดคล้องกับ Native UI ก็ทำยาก อาจจะมีบางตัวที่ทำได้เช่น Kendo แต่ก็ไม่ได้ลื่นไหลอยู่ดี

2) Native Cross Platform - ใช้ภาษาอะไรบางอย่าง เช่น C, Java, C# พัฒนาแอพฯขึ้นมาแล้วคอมไพล์เป็น Native ข้อดีคือประสิทธิภาพดีเยี่ยมเพราะเป็น Native ข้อเสียคือส่วนใหญ่พวกนี้จะมี Runtime มาครอบอีกทีหนึ่ง และการที่มันไม่ได้ลงไประดับ Native โดยตรง แต่ผ่าน Runtime พวกนี้ ฟีเจอร์เลยจะถูกจำกัดด้วย Runtime ของเครื่องมือนั้นๆ พอมีอะไรใหม่ๆในแพลตฟอร์มก็มักจะใช้ไม่ได้ หรืออีกนานกว่าจะได้ใช้ และบางทีถ้ามันมีบั๊กกับบางรุ่น (เช่นแอนดรอยด์เนี่ย เจอบ่อย) ก็ต้องสวดมนต์ภาวนาให้เจ้าของเครื่องมือแก้ให้สักที เราทำอะไรไม่ได้

ก็จะเห็นได้ว่าทั้ง Hybrid และ Native Cross Platform มีข้อจำกัดของตัวเองอยู่ ไม่ได้เหมาะกับทุกอย่าง โดยเฉพาะอย่างยิ่งเรื่องของการทำ UI ที่ทำให้เป็น Native ไม่ได้ จึงเหมาะกับการทำเกมซะมากกว่า (ถ้าทำเกมแนะนำ Unity3D เลย)

จึงเกิดเป็นแนวคิด Code Sharing ที่ไว้แชร์โค้ด "เป็นบางส่วน" ระหว่างแพลตฟอร์มขึ้นมา เพราะปกติโครงสร้างโค้ดอย่างหนึ่งที่เหมาะกับการทำแอพฯมือถือคือ MVC (Model-View-Controller) (ซึ่งจะไม่ขอพูดว่ามันคืออะไร เชื่อว่า Developer รู้จักกันอยู่แล้ว)

ซึ่งถ้าแยกออกมา ในส่วนของ Model เป็นส่วนของการคิดคำนวณ ไม่ใช่สิ่งที่จำกัดแพลตฟอร์ม จึงสามารถใช้ร่วมกันได้ไม่ว่าจะบนแพลตฟอร์มไหน แต่กับส่วนของ View (UI) เป็น Platform Specific ทำให้ไม่สามารถแชร์กันได้โดยตรง ส่วน Controller จะเป็นโครงหลักของแอพฯที่ต้องเขียนแยกกันตามแพลตฟอร์มเช่นกัน

และนี่คือแนวคิดของ Xamarin ที่ประกาศชัดว่า "ตัวเองไม่ใช่ Write Once Run Anywhere" แต่จะใช้วิธีสร้าง Core ขึ้นมาแชร์กันตรงกลาง (ADO, Manager, Library, etc.) และแตกส่วน View-Controller ออกเป็นแต่ละแพลตฟอร์มด้วยและเขียนด้วยโครงสร้างภาษาแบบเดียวกับ Native เช่นบนแอนดรอยด์ก็ต้องเขียนด้วย xml ใช้ findViewById เพื่อเข้าถึง View นั้นๆ มีการเรียกใช้ Adapter อะไรครบถ้วนเหมือนตัว Native Android ทุกประการ

และจากนั้นโค้ดก็จะถูกคอมไพล์เป็น Native และทำงานได้อย่างลื่นไหล Perfect มาก

ซึ่งแน่นอน โค้ดทั้งหมดนี้ไม่สามารถเอาไปคอมไพล์ลง iOS ได้โดยตรง ถ้าจะเอาลง iOS ก็ต้องแตก Project เพิ่มขึ้นมาใน Solution แล้วเขียน UI อีกชุดด้วย Storyboard และ xib

ซึ่งถ้าเขียนสำหรับ iOS และ Android พร้อมกันในโปรเจคเดียว ตัว Solution ก็จะมี 3 Projects อยู่ภายในด้วยกัน ได้แก่ ตัว Shared Project 1 โปรเจค และตัวโปรเจคหลักสำหรับ iOS & Android อย่างละโปรเจค หรือถ้าจะสนับสนุนแพลตฟอร์มมากกว่านี้ก็แตกโปรเจคเพิ่มกันปายยย

ถึงจะเขียนหัวข้อว่าแนวคิดแปลกใหม่ แต่จริงๆมันก็ไม่ได้แปลกใหม่หรอก มันมีมานานและแนวคิดนี้ แต่คิดว่าคนที่พัฒนาแอพฯมือถือจะไม่ค่อยรู้จักแนวคิดนี้กัน เลยเขียนว่าแปลกใหม่เพื่อให้ดูน่าสนใจขึ้น ... เป็นไง น่าสนใจขึ้นช่ะะะ

ใช้ C# เขียน แต่ต้องรู้วิธีเขียน Native ของแต่ละแพลตฟอร์มด้วย

Xamarin มีภาษาในการเขียนเป็น C# ซึ่งส่วนตัวต้องยอมรับว่าเป็นตัวเลือกที่ดีเพราะ C# เป็นภาษาที่ง่ายและเรียนรู้ได้อย่างรวดเร็ว

ในส่วนของการจัดการ UI อย่างที่บอกคือเราจะต้องใช้ภาษาที่เป็น Native ในการทำ เช่น Android ก็เขียนเป็น xml และเข้าถึงด้วย findViewById แม้แต่โครงของ UI ก็ยังต้องเป็น Activity ตัวอย่างโค้ดจะเป็นแบบนี้

using System.Collections.Generic;using Android.App;using Android.Content;using Android.OS;using Android.Widget;using Tasky.Core;using TaskyAndroid;namespace TaskyAndroid.Screens {	/// <summary>	/// Main ListView screen displays a list of tasks, plus an [Add] button	/// </summary>	[Activity (Label = "Tasky", MainLauncher = true, Icon="@drawable/icon")]				public class HomeScreen : Activity {		Adapters.TaskListAdapter taskList;		IList<Task> tasks;		Button addTaskButton;		ListView taskListView;				protected override void OnCreate (Bundle bundle)		{			base.OnCreate (bundle);			// set our layout to be the home screen			SetContentView(Resource.Layout.HomeScreen);			//Find our controls			taskListView = FindViewById<ListView> (Resource.Id.TaskList);			addTaskButton = FindViewById<Button> (Resource.Id.AddButton);			// wire up add task button handler			if(addTaskButton != null) {				addTaskButton.Click += (sender, e) => {					StartActivity(typeof(TaskDetailsScreen));				};			}						// wire up task click handler			if(taskListView != null) {				taskListView.ItemClick += (object sender, AdapterView.ItemClickEventArgs e) => {					var taskDetails = new Intent (this, typeof (TaskDetailsScreen));					taskDetails.PutExtra ("TaskID", tasks[e.Position].ID);					StartActivity (taskDetails);				};			}		}				protected override void OnResume ()		{			base.OnResume ();			tasks = TaskManager.GetTasks();						// create our adapter			taskList = new Adapters.TaskListAdapter(this, tasks);			//Hook up our adapter to our ListView			taskListView.Adapter = taskList;		}	}}

ใครเขียนแอนดรอยด์มาก็น่าจะตาลุกวาวเลย เพราะโครงสร้างมันช่างเหมือนกับการเขียนบน Android เสียนี่กะไร มาครบเลย Activity, SetContentView, FindViewById แม้แต่ Life Cycle อย่าง onCreate และ onResume

เมื่อมาในแนวนี้แล้ว แน่นอนว่าพวก Service, IntentService, BroadcastReceiver อะไรก็ใช้ได้หมด

[Service][IntentFilter(new String[]{"com.xamarin.DemoIntentService"})]public class DemoIntentService: IntentService{        public DemoIntentService () : base("DemoIntentService")        {        }        protected override void OnHandleIntent (Android.Content.Intent intent)        {                Console.WriteLine ("perform some long running work");                ...                Console.WriteLine ("work complete");        }}
[BroadcastReceiver][IntentFilter (new[] {Intent.ActionBootCompleted})]class BootCompletedBroadcastMessageReceiver : BroadcastReceiver{    public override void OnReceive(Context context, Intent intent)    {        if (intent.Action == Intent.ActionBootCompleted)        {            Toast.MakeText(context, "Hello - the app has hit the right place - now to launch the app", ToastLength.Long).Show();            context.StartActivity(typeof(MainActivity));        }    }}

พูดง่ายๆเลยนะ เหมือนเราเขียนแอพฯ Android แบบ Native 100% แต่แทนที่จะเป็นภาษา Java ก็เขียนด้วย C# แทน

พูดอีกนัยหนึ่ง ... เราต้องเขียนแอนดรอยด์เป็นก่อนและต้องเข้าใจทุกอย่างของแอนดรอยด์เป็นอย่างดี เช่น LifeCycle, Memory Management ถึงจะใช้ Xamarin ได้อย่างสมบูรณ์แบบ

เช่นเดียวกับ iOS มีวิธีเขียนหลายแบบ แบบหนึ่งก็คือโครงเดียวกับ Obj-C/Swift เลย มี AppDelegate และ ViewController ที่ LifeCycle ตรงเป๊ะเช่นกัน

using System;using System.Drawing;using MonoTouch.Foundation;using MonoTouch.UIKit;namespace PhonewordiOS{	public partial class PhonewordiOSViewController : UIViewController	{		static bool UserInterfaceIdiomIsPhone {			get { return UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; }		}		public PhonewordiOSViewController (IntPtr handle) : base (handle)		{		}				public override void DidReceiveMemoryWarning ()		{			// Releases the view if it doesn't have a superview.			base.DidReceiveMemoryWarning ();		}				#region View lifecycle		string translatedNumber = "";		public override void ViewDidLoad ()		{			base.ViewDidLoad ();			CallButton.Enabled = false;			PhoneNumberText.ShouldReturn += (textField) => {				textField.ResignFirstResponder();				return true;			};			PhoneNumberText.ClearsOnBeginEditing = true;			TranslateButton.TouchUpInside += (sender, e) => {				// *** SHARED CODE ***				translatedNumber = Core.PhonewordTranslator.ToNumber(PhoneNumberText.Text);				if (translatedNumber == "") {					CallButton.SetTitle ("Call ", UIControlState.Normal);					CallButton.Enabled = false;				} else {					CallButton.SetTitle ("Call " + translatedNumber, UIControlState.Normal);					CallButton.Enabled = true;				}			};			CallButton.TouchUpInside += (sender, e) => {				NSUrl url = new NSUrl("tel:" + translatedNumber);				if (!UIApplication.SharedApplication.OpenUrl(url))				{					var av = new UIAlertView("Not supported"					                         , "Scheme 'tel:' is not supported on this device"					                         , null					                         , "OK"					                         , null);					av.Show();				}			};		}				public override void ViewDidUnload ()		{			base.ViewDidUnload ();			ReleaseDesignerOutlets ();		}				public override void ViewWillAppear (bool animated)		{			base.ViewWillAppear (animated);		}				public override void ViewDidAppear (bool animated)		{			base.ViewDidAppear (animated);		}				public override void ViewWillDisappear (bool animated)		{			base.ViewWillDisappear (animated);		}				public override void ViewDidDisappear (bool animated)		{			base.ViewDidDisappear (animated);		}				#endregion				public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)		{			// Return true for supported orientations			if (UserInterfaceIdiomIsPhone) {				return (toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown);			} else {				return true;			}		}	}}

และเช่นกัน ... ต้องเขียนแอพฯ iOS เป็นและเข้าใจ LifeCycle อย่างดีถึงจะทำแอพฯ iOS ด้วย Xamarin ได้ดีครับ

สรุปนะสรุป ... อาจจะไม่ต้องกับเขียน Native เป็นถึงจะมาใช้ Xamarin ได้ แต่ก็ต้องรู้มาพอสมควรและเข้าใจมันเป็นอย่างดี ไม่งั้นก็เจ๊งงงง

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

ใช้พวก Native Library ได้ รวมถึง Android Support Library

อย่างที่เห็น ทุกอย่างมันช่าง Native ดังนั้นไม่ต้องแปลกใจที่เราจะใช้พวก Native Library ได้หมด

เป็นอีกจุดแข็งที่เหนือกว่าการทำ Cross Platform แบบ Write Once Run Anywhere เพราะอันนี้มันลง Native ได้โหดมาก จะใช้อะไรมีให้ใช้หมด แบบว่าหมดจริงๆ

อัพเดต Library เร็วมาก

ต้องชมทีม Xamarin ที่คอยอัพเดต Library อยู่อย่างต่อเนื่อง มีอะไรใหม่มาปุ๊บ อีกไม่เกิน 1 อาทิตย์จะได้เห็นบน Xamarin แล้ว อย่างเช่น Android L ก็เขียนได้ด้วย Xamarin หรือตอนที่ iOS 8 ออกใหม่ๆ ทุก beta ที่ออกมา Xamarin อัพตามหมด

เขียนแอพฯสำหรับ iOS บน Windows ได้ (ประมาณว่าได้)

อีกจุดแข็งที่ดูค่อนข้างเหนือชั้นเลยทีเดียวคือ เราสามารถเขียนโค้ด iOS บน Windows ได้ด้วย โดยใช้ Xamarin.iOS for Visual Studio Plugin ที่เพิ่มความสามารถให้กับ Visual Studio (Prefessional ขึ้นไปเท่านั้น) ในการเขียนโค้ดสำหรับ iOS อย่างสมบูรณ์แบบ รวมถึงการทำ storyboard ด้วย

แต่ในเรื่องการคอมไพล์ ยังไงก็ไม่สามารถคอมไพล์บน Windows ได้ เพราะตัว Compiler มีอยู่บน OS X เท่านั้น ทาง Xamarin แก้ปัญหาด้วยวิธี Remote Compiling โดยเราต้องมีเครื่อง Mac วางไว้เป็น Build Host เครื่องนึง (หรือจะรัน VM เอาก็ได้) แล้วเวลาจะคอมไพล์ Xamarin.iOS for Visual Studio จะจัดแจงส่งโค้ดไปคอมไพล์บนเครื่องแมคนั้นให้ ถ้าจะทดสอบบน Simulator มันก็จะส่งเป็นภาพกลับมาให้แบบ Remote Desktop เพราะ iOS Simulator รันบน Windows ไม่ได้นั่นเอง

อีกข้อจำกัดคือ Deploy ลง Device จริงไม่ได้ แต่ก็ยังสามารถสร้างเป็นไฟล์ ipa ได้อยู่ เอา ipa นั้นมาลงผ่าน XCode อีกทีได้อยู่ แต่จะ Debug บน Device คงทำไม่ได้ (นอกจากเอา Device ไปเสียบกับเครื่อง Mac นั้นอยู่ ซึ่ง ... ถ้าขนาดนั้นแล้วไม่ใช้ Mac เขียนไปเลยหละ)

สรุปนะ ... ใช้ Mac เขียนเห๊อะ ...

โครงสร้างไฟล์ที่ถูกคอมไพล์แล้ว

แอพฯที่ถูกคอมไพล์แล้วจะประกอบด้วย 3 ส่วนคือ Mono Runtime (ขนาดคงที่), BCL (Base Class Library) ที่ขนาดแปรเปลี่ยนไปตาม Library ที่เราเรียกใช้ และ Compiled User Code (IL) คือส่วนของโค้ดที่เราเขียนขึ้นมา เป็นไปตามสีเขียน ฟ้าและส้ม ตามลำดับ

ขนาดของแอพฯเริ่มต้นก็เลยจะใหญ่สักหน่อย เป็นหลัก MB เลย

ไม่ฟรี

เอา Pricing Plan มาให้ดู

จริงอยู่ที่มันมี Starter Edition ซึ่งฟรีอยู่ แต่ข้อจำกัดคือมันจำกัดส่วน Compiled User Code (IL) ไว้ที่ 64KB เท่านั้น ... ใช่แล้ว 64KB เท่านั้น ... พูดง่ายๆเลยคือมันเอาไปใช้จริงไม่ได้ ขนาดแค่นั้นอ่ะ

ดังนั้นถ้าจะเขียนโปรแกรมจริงจังยังไงก็ต้องจ่าย ราคาเริ่มต้นคือแพลน Indie เดือนละ $25 ต่อเครื่อง ถ้าพัฒนา 4 คน 4 เครื่องก็เดือนละ $100 ...

ฟรีแล้ว

อัปเดตล่าสุด หลังจากที่ Microsoft ซื้อ Xamarin ไป ตอนนี้ Xamarin ก็ปล่อยให้ทุกคนได้ใช้ฟรีเป็นที่เรียบร้อย ถือว่าเป็นเรื่องที่พลิกจากไม่น่าใช้กลายเป็นน่าใช้ไปได้เลย พูดถึงต้นทุนตอนนี้ไม่มีแล้วครับ ใช้ได้เลย =)

ใช้งานจริงได้หรือยัง?

ใช้งานจริงได้แล้วครับ หากทีมคุณโอเคก็ย้ายมาใช้ Xamarin ได้เลย ไม่มีปัญหาเรื่องเทคนิคใดๆ

Xamarin เหมาะกับอะไร? ใครควรใช้ ใครไม่ควร?

สาธยายมายาวมาก ก็คงจะเห็นว่า Xamarin ไม่เหมาะกับผู้เริ่มต้นเท่าไหร่นัก ถ้าให้แนะนำคงแนะนำว่าถ้าเป็นบริษัทเล็กๆ ผลงานไม่ได้ใหญ่โต ทีมไม่ได้ใหญ่มากมาย ก็อย่าไปใช้ Xamarin เลย ไปใช้ Native น่าจะดีกว่า เพราะ Code Sharing มันก็ไม่ได้แชร์ได้เยอะมากขนาดนั้น อย่างมากก็ 30% ของโค้ดหละมั้ง

โดยเฉพาะอย่างยิ่งตอนนี้นาทีที่สงครามจบแล้ว มีแพลตฟอร์มที่เหลือรอดแบบควรโฟกัสแค่ iOS และ Android ทางด้าน Android ถ้าเขียนแบบ Xamarin มันก็เหมือนเขียนด้วย Java ปกติเลย เพราะ C# กับ Java ไม่ได้ต่างกันมาก แล้ว iOS ก็ออก Swift ซึ่งเขียนง่ายมากๆมาแล้ว ดูๆแล้วถ้าเขียนแยกยังอาจจะออกแรงน้อยกว่าเสียอีก

แล้วถามว่ามันเหมาะกับอะไร? ข้อดีของ Xamarin คือมันเป็นภาษาเดียวคือ C# ดังนั้นข้อได้เปรียบคือเรื่องของ "การสื่อสารภายในทีม"

ดังนั้น Xamarin คงเหมาะกับทีมที่ใหญ่สักหน่อย ใหญ่พอจะเกิดปัญหาด้านการสื่อสารได้ การใช้ Xamarin จะช่วยแก้จุดอ่อนตรงนี้ได้ แต่ก็ต้อง Trade Off กับการต้องเรียนรู้สิ่งใหม่ (ก็คือภาษา C# บน Xamarin และวิธีเขียน) และอาจจะลำบากหน่อยในเรื่องของ Community หากมีปัญหาก็อาจจะแก้ได้ยากกว่าเขียน Native

ย้ำอีกทีว่าประโยชน์ของ Xamarin ไม่ใช่เรื่องการทำให้โค้ดสั้นลง แต่เป็นการทำให้การสื่อสารภายในทีมง่ายขึ้นจากการวางทุกอย่างให้เป็นภาษาเดียว (เฉพาะส่วน Shared Code)

ถ้าถามเนย Most of the case ถ้าคิดจะใช้ Xamarin คงแนะนำให้เขียน Native แยกยังจะดีกว่า แต่ปัจจัยมันก็หลายอย่างอ่าโนะ ที่เหลือก็ลองพิจารณาว่าเหมาะกับทีมหรือบริษัทคุณหรือเปล่าดูกันจ้า


จบจ๊ะะะะะ

บทความที่เกี่ยวข้อง

Oct 26, 2014, 20:05
17894 views
กว่าจะมาเป็นแอพฯมือถือดีๆสักตัว ทีมต้องประกอบด้วยคนที่มีสกิลดังต่อไปนี้ ...
Oct 17, 2014, 19:04
7004 views
มันกลับมาแล้ว "ขนมโคนจิ๋ว" ขนมย้อนยุค สำหรับคนรุ่นที่รักนั้นยิ่งใหญ่กว่าดินน้ำลมไฟ
0 Comment(s)
Loading