Computer >> Hướng Dẫn Máy Tính >  >> Hệ Thống >> Android

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Series ứng dụng Kriptofolio - Phần 2

Làm sao thực sự bắt đầu xây app mới? Nước đi đầu tiên là gì? Nếu nghĩ chỉ cần mở Android Studio và nhảy vào code, hãy suy nghĩ lại. Đó chính là điều tôi khuyên không nên làm — nó gây hại hơn là tốt. Nhưng rất cám dỗ để viết dòng code đầu sớm nhất.

Thay vào đó, tôi khuyên tập trung làm kế hoạch khôn ngoan với UI mockup. Mọi project app tốt đều nên bắt đầu như vậy. Cách này không tốn nhiều thời gian và xây được sản phẩm chất lượng cao từ đầu.

Phần này trình bày mockup app "Kriptofolio" (trước "My Crypto Coins") và cách tạo chúng. Cũng xây dựng tất cả UI layout. Layout trở thành foundation vững chắc chỉ rõ gì để code. Cuối cùng localize app sang nhiều ngôn ngữ và xử lý ngôn ngữ viết right-to-left.

Nội dung Series

  • Giới thiệu: Lộ trình xây dựng app Android hiện đại 2018–2019
  • Phần 1: Giới thiệu nguyên lý SOLID
  • Phần 2: Bắt đầu xây dựng app Android: Mockup, UI, XML layouts (bạn đang ở đây)
  • Phần 3: Tất cả về kiến trúc: khám phá pattern và cách dùng
  • Phần 4: Triển khai Dependency Injection với Dagger 2
  • Phần 5: Xử lý RESTful Web Services với Retrofit, OkHttp, Gson, Glide và Coroutines

Mockup

Có nhiều cách tạo project mockup. Cách đơn giản: bút chì và giấy, bắt đầu vẽ. Ưu điểm: không tốn tiền, bắt đầu ngay. Quên mất, cần cục tẩy vì không có undo. 😄

Nếu cần chức năng nhiều hơn, cân nhắc software tạo mockup chi tiết. Tôi thích dùng software hơn bút giấy, dù tốn tiền mua và thời gian học.

Có nhiều software trên thị trường. Tự tìm hiểu cái nào phù hợp nhu cầu. Cho mọi project mockup hiện tại tôi dùng Balsamiq Mockups for Desktop. Balsamiq nhanh, hiệu quả, dễ dùng wireframing software. Vì hài lòng, khuyến nghị cho Android app, hãy thử.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Tôi bắt đầu My Crypto Coins app bằng mockup suy nghĩ kỹ, chi tiết. Nghĩ rằng nếu tạo mọi thứ chi tiết, sẽ tránh lỗi. Cũng không lãng phí thời gian thay đổi functionality giữa chừng dev. Nếu đầu tư effort vào mockup tốt, với chút tưởng tượng, nhìn thấy và cảm nhận sản phẩm cuối.

Mục tiêu: mọi thứ định nghĩa trong mockup như sản phẩm cuối. Không vội, dành nhiều thời gian cần thiết. Mockup cuối cho My Crypto Coins app:

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Mockup My Crypto Coins app bằng Balsamiq Mockups software

Thiết kế chuẩn — Material

Điều quan trọng khác: visual design app. Thời điểm viết, Material Design là visual design chuẩn Google khuyến nghị cho mọi Android app. Và đoán gì — không thể sai với chuẩn.

My Crypto Coins app dùng Material Design. Tuân thủ best practices trong guidelines online chi tiết.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Material.io/guidelines — spec document chính thức Material Design, guideline thiết kế cho project.

Layouts

Đã có wireframe, đến lúc build UI thật. Lại muốn nhảy vào code, nhưng tại sao vội? Thay vào đó, tập trung build tất cả XML layout. Lời khuyên: càng nhiều thứ đưa vào XML, càng ít code viết.

Android team liên tục cải thiện cách build UI bằng tính năng mới XML layout. Đôi khi cám dỗ viết code vài dòng cho giao diện mong muốn. Nhưng tốt hơn điều tra sâu xem có làm được không bằng XML. Nhớ: ít code = project clean hơn. Dễ hiểu cho dev khác, dễ maintain.

Cuối cùng khi tạo xong XML layout, cảm giác như đã có app. Launch được và cảm nhận trải nghiệm user. Không quan trọng là fake data và không làm gì. Đây là cơ hội cuối làm thay đổi lớn.

Nếu build app cho người khác, lúc này present rất tốt. Có thể yêu cầu thay đổi last minute. Cách này tránh viết code cho functionality không bao giờ dùng. Nhiều người thiếu tưởng tượng, cần nhìn và chạm mới quyết định. Đừng để layout thành bước cuối trong quy trình.

App này dùng các component phổ biến trong Android app hiện đại:

  • CoordinatorLayout — FrameLayout siêu mạnh, điểm hấp dẫn: điều phối animation/transition view bên trong.
  • AppBarLayout — LinearLayout dọc implement nhiều feature material design app bar, cuộn gesture.
  • Toolbar — generalization của action bars dùng trong app layout.
  • CollapsingToolbarLayout — wrapper cho Toolbar implement collapsing app bar. Dùng như direct child của AppBarLayout.
  • ConstraintLayout — ViewGroup cho phép position và size widget linh hoạt. RelativeLayout trên steroid.
  • SwipeRefreshLayout — widget implement hoàn toàn swipe-to-refresh pattern. Detect vertical swipe, hiển thị progress bar, trigger callback.
  • FloatingActionButton — button tròn trigger primary action trong UI app.

Màn hình chính dùng kết hợp tất cả component tạo UI/UX đẹp. User expand/collapse phần trên layout xem total value portfolio crypto. Check thay đổi giá trị 24h và đổi fiat currency.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Tạo layout màn hình chính

Tồn tại component khác ngoài 9 cái này. Quan trọng master toàn bộ component palette để biết dùng cái nào cho case nào. Với mobile trend thay đổi, palette cũng mở rộng.

Sẽ không nói từng component, khuyến nghị tự điều tra. Cách tôi hiểu tốt nhất: build XML layout thủ công thay vì drag & drop auto Android Studio.

Styles, Colors, Dimensions, Strings, Icons

Nhìn đầu có vẻ không quan trọng. Tuy nhiên, tôi xin bạn nỗ lực tạo chúng trong mọi app nếu muốn modern và exclusive. Nhớ series này là cách build MODERN Android app! Dù sai, người ta thường đánh giá app bằng nhìn trước, không phải functionality.

Cơ hội hoàn hảo kiếm tình yêu user từ đầu. Mẹo của tôi:

  • Build XML layout, nhận ra lặp attribute chung cho view. Di chuyển định nghĩa thành XML styles riêng. Layout XML ngắn gọn hơn. Kiểm soát styling app từ nơi riêng. Tưởng tượng lợi ích: cho user chọn app skin (bright, dark...).
  • Định nghĩa tất cả màu app trong file riêng, tận hưởng thử nghiệm look bằng đổi màu ngay. Đôi khi sản phẩm cũ được sống lại và engage user lại chỉ bằng refresh màu. Có vài website giúp chọn màu đẹp, yêu thích MaterialPalette.com, xem ngay.
  • Định nghĩa dimension app trong file riêng. Cho phép adjust app đẹp trên màn hình size khác nhau.
  • Tất cả strings không hard-code, Android Studio báo nếu quên. Đừng bỏ qua. Phần hay khi tách string: dịch app sang ngôn ngữ khác.
  • Dùng icon cho app, luôn ưu tiên XML vector drawable format. Chuẩn mới khuyến nghị, cách thông minh tránh pixelation. Tìm icon material design pro từ cộng đồng tại MaterialDesignIcons.com. Tôi dùng site này cho icon My Crypto Coins app.

RecyclerView

Màn hình chính My Crypto Coins app gồm list cryptocurrency user nắm giữ. RecyclerView widget phù hợp nhất. Widget hiển thị scrolling list element dựa trên large data set (hoặc data thường thay đổi).

Nhờ ưu điểm, RecyclerView khuyến nghị cho mọi list screen. Phiên bản advanced, flexible hơn ListView đơn giản. Sẽ nói sau. Phần này chỉ tạo layout, không tập trung code.

Để thấy app trực quan, phải implement RecyclerView bằng code. Các bước:

1. Thêm RecyclerView component.

Layout MainActivity là activity_main.xml. Include content_main.xml là fragment. MainActivityListFragment inflate fragment_main_list.xml. Thêm RecyclerView ở đây.

... <android.support.v7.widget.RecyclerView android:id="@+id/recyclerview_fragment_main_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorForMainListBackground" android:clipToPadding="false" android:paddingBottom="72dp" android:paddingTop="5dp" android:scrollbarStyle="outsideOverlay" android:scrollbars="vertical" /> ...

Cấu hình RecyclerView để padding bottom tránh che item cuối bởi FloatingActionButton. Bật vertical scrollbar.

2. Tạo RecyclerView row layout.

Mục đích ban đầu chỉ set item name cho mỗi row. Layout đơn giản:

<android.support.v7.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/main_cardview_list_item_outer_top_bottom_margin" android:layout_marginEnd="@dimen/main_cardview_list_item_outer_start_end_margin" android:layout_marginStart="@dimen/main_cardview_list_item_outer_start_end_margin" android:layout_marginTop="@dimen/main_cardview_list_item_outer_top_bottom_margin" android:foreground="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" app:cardBackgroundColor="@color/colorForMainListItemBackground"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="@dimen/main_cardview_list_item_inner_margin"> ... <android.support.v7.widget.AppCompatTextView android:id="@+id/item_name" style="@style/MainListItemPrimeText" android:layout_marginEnd="@dimen/main_cardview_list_item_text_between_margin" android:layout_marginStart="@dimen/main_cardview_list_item_inner_margin" app:layout_constraintBottom_toTopOf="@+id/item_amount_symbol" app:layout_constraintEnd_toStartOf="@+id/guideline1_percent" app:layout_constraintStart_toEndOf="@+id/item_image_icon" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="spread" tools:text="@string/sample_text_item_name" /> ... </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView>

3. Tạo data adapter class.

Adapter tạm nhận string data. Sau cần class data model riêng. Cần pass nhiều info hơn một string.

class MainRecyclerViewAdapter(val dataList: ArrayList<String>) : RecyclerView.Adapter<MainRecyclerViewAdapter.CustomViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder { val v = LayoutInflater.from(parent.context).inflate(R.layout.fragment_main_list_item, parent, false) return CustomViewHolder(v) } override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { holder.txtName?.text = dataList[position] } override fun getItemCount(): Int { return dataList.size } inner class CustomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val txtName = itemView.findViewById<TextView>(R.id.item_name) } }

4. Kết nối RecyclerView với custom adapter.

class MainActivityListFragment : Fragment() { private lateinit var recyclerView: RecyclerView private lateinit var recyclerAdapter: MainRecyclerViewAdapter override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val v: View = inflater.inflate(R.layout.fragment_main_list, container, false) recyclerView = v.findViewById(R.id.recyclerview_fragment_main_list) return v } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupList() } private fun setupList() { val data = ArrayList<String>() data.add("Bitcoin") data.add("Etherium") data.add("Ripple") data.add("Bitcoin Cash") data.add("Litecoin") data.add("NEO") data.add("Stellar") data.add("EOS") data.add("Cardano") data.add("Stellar") data.add("IOTA") data.add("Dash") data.add("Monero") data.add("TRON") data.add("NEM") data.add("ICON") data.add("Bitcoin Gold") data.add("Zcash") data.add("Verge") recyclerView.layoutManager = LinearLayoutManager(activity) recyclerAdapter = MainRecyclerViewAdapter(data) recyclerView.adapter = recyclerAdapter } }

Xong! Màn hình chính trông thế này:

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Màn hình chính

ListView

Dự án này dùng ListView cho màn hình add crypto coin đã nắm giữ. Gần đây ít dùng do hạn chế.

Nhiều bạn bây giờ thắc mắc tại sao dùng ListView khi RecyclerView làm dễ dàng cùng chức năng.

Nhớ project này tạo để training. Tôi nghĩ có ích hiểu ListView và cách hoạt động. Developer nào cũng có thể gặp ListView trong legacy code, tốt nhất biết cách làm việc. Bên cạnh đó, list tạo rất đơn giản, hạn chế kỹ thuật ListView không gây vấn đề.

Làm theo bước tương tự implement ListView:

1. Thêm ListView component.

Thêm ListView vào AddSearchActivity. Mở layout activity activity_add_search.xml, thấy include content_add_search.xml. Thêm ListView ở đó.

... <ListView android:id="@+id/listview_activity_add_search" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="vertical" /> ...

2. Tạo ListView row layout.

Giống trước, chỉ set item name cho row. Layout đơn giản:

<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="@dimen/add_search_list_item_inner_margin"> ... <android.support.v7.widget.AppCompatTextView android:id="@+id/item_name" style="@style/AddSearchListItemPrimeText" android:layout_marginEnd="@dimen/add_search_list_item_text_between_margin_2x" android:layout_marginStart="@dimen/add_search_list_item_text_between_margin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/item_symbol" app:layout_constraintStart_toEndOf="@+id/item_image_icon" app:layout_constraintTop_toTopOf="parent" tools:text="@string/sample_text_item_name" /> ... </android.support.constraint.ConstraintLayout>

3. Tạo data adapter class.

Giống RecyclerView, ListView adapter tạm nhận string data để show item name. Sau dùng data model class riêng. Phần này muốn list đơn giản chỉ hiển thị title cryptocurrency. Thay vì custom adapter, có thể dùng ArrayAdapter mặc định.

class AddSearchListAdapter(context: Context, private val dataSource: ArrayList<String>) : BaseAdapter() { private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { val view: View val holder: CustomViewHolder if (convertView == null) { view = inflater.inflate(R.layout.activity_add_search_list_item, parent, false) holder = CustomViewHolder() holder.nameTextView = view.findViewById(R.id.item_name) view.tag = holder } else { view = convertView holder = convertView.tag as CustomViewHolder } val nameTextView = holder.nameTextView nameTextView.text = getItem(position) as String return view } override fun getItem(position: Int): Any { return dataSource[position] } override fun getItemId(position: Int): Long { return position.toLong(); } override fun getCount(): Int { return dataSource.size } inner class CustomViewHolder { lateinit var nameTextView: AppCompatTextView } }

Trong adapter code, tạo object CustomViewHolder dùng ViewHolder pattern. Lưu reference view row list. Gọi findViewById() chỉ vài lần. Cho list scroll mượt và hiệu quả.

ListView không bắt buộc ViewHolder pattern. RecyclerView adapter mặc định ép dùng nó.

4. Kết nối ListView với custom adapter.

Với ArrayAdapter mặc định: val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, data). Đó là vẻ đẹp ListView. Nếu muốn, tạo list nhanh thật mà không cần custom adapter hay row layout (bỏ bước 2, 3).

class AddSearchActivity : AppCompatActivity() { private lateinit var listView: ListView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_add_search) setSupportActionBar(toolbar2) supportActionBar?.setDisplayHomeAsUpEnabled(true) val data = ArrayList<String>() data.add("Bitcoin") data.add("Etherium") data.add("Ripple") data.add("Bitcoin Cash") data.add("Litecoin") data.add("NEO") data.add("Stellar") data.add("EOS") data.add("Cardano") data.add("Stellar") data.add("IOTA") data.add("Dash") data.add("Monero") data.add("TRON") data.add("NEM") data.add("ICON") data.add("Bitcoin Gold") data.add("Zcash") data.add("Verge") val adapter = AddSearchListAdapter(this, data) listView = findViewById(R.id.listview_activity_add_search) listView.adapter = adapter } ... }

ListView setup xong!

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Màn hình add crypto coin

SearchView

Cùng màn hình list cryptocurrency, cũng cần SearchView. Search hữu ích cho user tìm crypto cụ thể bằng gõ tên. Phần này không build đầy đủ functionality, chỉ implement visual part. Các bước thêm SearchView:

1. Khai báo searchable configuration trong XML.

File searchable config đặt trong res/xml. Chỉ định attribute cho SearchView định nghĩa behavior.

<searchable xmlns:android="https://schemas.android.com/apk/res/android" android:label="@string/app_name" android:hint="@string/search_hint"> </searchable>

2. Tạo activity mới thành searchable activity.

Tạo blank activity extends AppCompatActivity(). Đặt tên AddSearchActivity.

3. Chỉ định activity mới trong Android manifest thành searchable.

<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="com.baruckis.mycryptocoins"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> ... <activity android:name=".AddSearchList.AddSearchActivity" android:launchMode="singleTop" android:parentActivityName=".MainList.MainActivity" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.SEARCH" /> </intent-filter> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" /> </activity> ... </application> </manifest>

Để Android system handle search process. Thêm intent action search và meta-data trong element activity AddSearchActivity. Meta-data có name và resource link đến searchable config file ở res/xml.

4. Tạo search menu.

Trong res/menu tạo menu resource file.

<menu xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto"> <item android:id="@+id/search" android:icon="@drawable/ic_search" android:title="@string/action_search" app:actionViewClass="android.support.v7.widget.SearchView" app:showAsAction="ifRoom|collapseActionView" /> </menu>

5. Thêm search menu vào activity.

Thêm Android search widget làm menu action view.

class AddSearchActivity : AppCompatActivity() { ... override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_search, menu) val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager val searchView = menu?.findItem(R.id.search)?.actionView as SearchView searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) searchView.maxWidth = Integer.MAX_VALUE return true } }

SearchView đã thêm vào activity. Chức năng search chưa hoạt động. Nhưng implement visual như mong đợi.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Màn hình add crypto — search functionality

Settings

Muốn tạo modern Android app, khuyến nghị có settings screen và cho user truy cập setting. Setting cho user quyền kiểm soát một số functionality, làm họ hạnh phúc hơn. Họ kiểm soát behavior app. Tạo settings screen cho My Crypto Coins app.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

New Settings Activity từ gallery template

Tạo settings screen với Android Studio template, sinh toàn bộ code cần. Mặc định thời điểm viết, settings activity sinh với preference headers. Không phù hợp app nhỏ chỉ vài setting ban đầu.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Default settings template với preference headers

Vì vậy build thủ công. Settings screen thiết kế với XML-like layout. Tạo visual part cho bài viết:

1. Tạo preferences screen XML file.

File preferences screen XML đặt trong res/xml.

<PreferenceScreen xmlns:android="https://schemas.android.com/apk/res/android" xmlns:tools="https://schemas.android.com/tools"> <PreferenceCategory android:title="@string/pref_general_category_title"> <ListPreference android:defaultValue="@string/pref_default_language_value" android:entries="@array/pref_language_list_entries" android:entryValues="@array/pref_language_list_values" android:icon="@drawable/ic_translate" android:key="language_list" android:summary="@string/pref_default_language_entry" android:title="@string/pref_language_title" tools:summary="@string/pref_default_language_entry" /> <ListPreference android:defaultValue="@string/pref_default_fiat_currency_value" android:entries="@array/pref_fiat_currency_list_entries" android:entryValues="@array/pref_fiat_currency_list_values" android:icon="@drawable/ic_cash" android:key="fiat_currency_list" android:summary="@string/pref_default_fiat_currency_entry" android:title="@string/pref_fiat_currency_title" tools:summary="@string/pref_default_fiat_currency_entry" /> <ListPreference android:defaultValue="@string/pref_default_date_format_value" android:entries="@array/pref_date_format_list_entries" android:entryValues="@array/pref_date_format_list_values" android:icon="@drawable/ic_date_range" android:key="date_format_list" android:summary="@string/pref_default_date_format_entry" android:title="@string/pref_date_format_title" tools:summary="@string/pref_default_date_format_entry" /> <SwitchPreference android:defaultValue="true" android:icon="@drawable/ic_calendar_clock" android:key="24h_switch" android:summary="@string/pref_24h_switch_summary" android:title="@string/pref_24h_switch_title" /> </PreferenceCategory> <PreferenceCategory android:title="@string/pref_support_category_title"> <Preference android:icon="@drawable/ic_star" android:title="@string/pref_rate_app_title" /> <Preference android:icon="@drawable/ic_share" android:title="@string/pref_share_app_title" /> <Preference android:icon="@drawable/ic_attach_money" android:summary="@string/pref_donate_view_summary" android:title="@string/pref_donate_view_title" /> <Preference android:icon="@drawable/ic_currency_btc" android:summary="@string/pref_donate_crypto_summary" android:title="@string/pref_donate_crypto_title" /> </PreferenceCategory> <PreferenceCategory android:title="@string/pref_support_about_title"> <Preference android:icon="@drawable/ic_web" android:summary="@string/pref_website_summary" android:title="@string/pref_website_title" /> <Preference android:icon="@drawable/ic_human_greeting" android:summary="@string/pref_author_summary" android:title="@string/pref_author_title" /> <Preference android:icon="@drawable/ic_github_circle" android:summary="@string/pref_source_summary" android:title="@string/pref_source_title" /> <Preference android:icon="@drawable/ic_file_multiple" android:title="@string/pref_open_source_title" /> <Preference android:icon="@drawable/ic_copyright" android:summary="@string/pref_license_summary" android:title="@string/pref_license_title" /> <Preference android:icon="@drawable/ic_info_outline" android:summary="@string/pref_app_summary" android:title="@string/pref_app_title" /> </PreferenceCategory> </PreferenceScreen>

2. Tạo preferences fragment.

Tạo blank fragment SettingsFragment extends PreferenceFragment(). Fragment tạo preferences từ XML resource tạo. Tương lai fragment chứa method inflate XML settings và callback khi setting thay đổi.

class SettingsFragment : PreferenceFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) addPreferencesFromResource(R.xml.pref_main); } }

3. Tạo preferences activity.

Settings fragment ready, tạo activity mới AppCompatPreferenceActivity extends PreferenceActivity(). Class cung cấp compatibility mọi device/version.

abstract class AppCompatPreferenceActivity : PreferenceActivity() { override fun onCreate(savedInstanceState: Bundle?) { delegate.installViewFactory() delegate.onCreate(savedInstanceState) super.onCreate(savedInstanceState) } override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) delegate.onPostCreate(savedInstanceState) } val supportActionBar: ActionBar? get() = delegate.supportActionBar fun setSupportActionBar(toolbar: Toolbar?) { delegate.setSupportActionBar(toolbar) } override fun getMenuInflater(): MenuInflater { return delegate.menuInflater } override fun setContentView(@LayoutRes layoutResID: Int) { delegate.setContentView(layoutResID) } override fun setContentView(view: View) { delegate.setContentView(view) } override fun setContentView(view: View, params: ViewGroup.LayoutParams) { delegate.setContentView(view, params) } override fun addContentView(view: View, params: ViewGroup.LayoutParams) { delegate.addContentView(view, params) } override fun onPostResume() { super.onPostResume() delegate.onPostResume() } override fun onTitleChanged(title: CharSequence, color: Int) { super.onTitleChanged(title, color) delegate.setTitle(title) } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) delegate.onConfigurationChanged(newConfig) } override fun onStop() { super.onStop() delegate.onStop() } override fun onDestroy() { super.onDestroy() delegate.onDestroy() } override fun invalidateOptionsMenu() { delegate.invalidateOptionsMenu() } private val delegate: AppCompatDelegate by lazy { AppCompatDelegate.create(this, null) } }

4. Tạo settings activity.

class SettingsActivity : AppCompatPreferenceActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupActionBar() fragmentManager.beginTransaction().replace(android.R.id.content, SettingsFragment()).commit() } private fun setupActionBar() { supportActionBar?.setDisplayHomeAsUpEnabled(true) } override fun onMenuItemSelected(featureId: Int, item: MenuItem): Boolean { val id = item.itemId if (id == android.R.id.home) { if (!super.onMenuItemSelected(featureId, item)) { NavUtils.navigateUpFromSameTask(this) } return true } return super.onMenuItemSelected(featureId, item) } }

5. Thêm settings item vào main menu.

<menu xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools" tools:context="com.baruckis.mycryptocoins.MainList.MainActivity"> <item android:id="@+id/action_settings" android:orderInCategory="100" android:title="@string/action_settings" app:showAsAction="never" /> </menu>

6. Khởi động settings activity khi chọn settings từ overflow menu.

class MainActivity : AppCompatActivity() { ... override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_settings -> { startActivity(Intent(this@MainActivity, SettingsActivity::class.java)); return true } else -> super.onOptionsItemSelected(item) } } }

7. Chỉ định settings activity trong Android manifest.

<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="com.baruckis.mycryptocoins"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> ... <activity android:name=".Settings.SettingsActivity" android:label="@string/title_activity_settings" android:parentActivityName=".MainList.MainActivity"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.baruckis.mycryptocoins.MainList.MainActivity" /> </activity> </application> </manifest>

Chúc mừng, cuối cùng launch settings từ toolbar menu item.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Settings screen

Dùng thư viện 3rd party — Gmail style flip circle button-view

Có ý kiến khác nhau về dùng thư viện 3rd party — code do dev khác vendor platform gốc tạo. Tránh dùng vì không rõ support bao lâu và tốt bao nhiêu. Người khác dùng vì đẩy nhanh dev.

Quyết định dùng phụ thuộc case cụ thể. Tự điều tra library xem có đủ flexible fit requirement không.

My Crypto Coins app có situation: muốn tạo circle image view. Image show icon crypto cụ thể trong hình tròn. Nếu icon không tồn tại, show 3 chữ cái đầu mã crypto.

Bên cạnh đó muốn select image bằng click. Selection present bằng short flip view animation.

Tất cả UX này không phải tôi nghĩ ra. Tìm thấy behavior tương tự trên Gmail app, trông rất tốt.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Gmail app flip circle view animation

Làm sao tạo nó? Thực ra circle image view không phải component mặc định trong Android Studio palette.

Có thể đi con đường khó: tự tạo mọi thứ, nhưng tốn thời gian/năng lượng. Thay vào đó, chọn implement một trong nhiều open source library tạo cho mục đích đó.

Cho yêu cầu UI/UX, tôi tìm library GitHub davideas/FlipView. Sau điều tra, quyết định library làm rất professional. Dễ implement, support đủ lâu để thử. Bắt đầu dùng theo bước dễ:

1. Thêm dependency.

Import library vào project.

dependencies { implementation 'eu.davidea:flipview:1.1.3' }

2. Sử dụng.

Cấu hình FlipView với attribute Android thông thường và custom app. Có dễ không?

<android.support.v7.widget.CardView xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" ... <eu.davidea.flipview.FlipView android:id="@+id/item_image_icon" style="@style/FlipView" android:clickable="true" android:focusable="true" app:layout_constraintBottom_toTopOf="@+id/item_ranking" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> ... </android.support.v7.widget.CardView>

Đừng phát minh lại bánh xe. Luôn cân nhắc dùng library trước để deliver nhanh hơn. Nếu project rối, không phải library cũ chịu trách nhiệm mà cách bạn cấu trúc code. Nếu project modular đủ thì không vấn đề. Đó là topic riêng, sẽ nói ở bài sau.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Flip View dùng trong main screen list

Localization với RTL support

Khi quyết định tạo app, tôi đặt mục tiêu không chỉ default English UI, mà dịch sang tiếng mẹ đẻ Lithuanian từ đầu dev.

Mục tiêu dẫn đến học localization Android. Khám phá: add support nhiều ngôn ngữ rất dễ. Như nói trước, tách tất cả string vào strings.xml. Sau đó launch Translations Editor tool trong Android Studio. Cho phép thêm language support mới.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Interface rất trực quan. Dịch từng string từ strings.xml sang ngôn ngữ mới. File mới sinh bởi IDE trong thư mục riêng. Ví dụ Lithuanian: values-lt/strings.xml. Xong! 😊

Chuyển system language thiết bị Android sang ngôn ngữ vừa dịch, chạy app, toàn bộ UI tự update translation. Hơn nữa, My Crypto Coins app sau plan thêm switch language runtime. Đó là lý do có language switcher trong settings.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Settings screen — language Lithuanian

Dịch app sang nhiều ngôn ngữ chắc chắn mở rộng audience. Tại sao không thêm support vài ngôn ngữ đặc biệt viết Right-to-Left (RTL) — Arabic, Hebrew, Persian.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Thực ra add RTL support không khó. Chỉ thêm attribute support vào manifest:

<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="com.baruckis.mycryptocoins"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> ... </application> </manifest>

Chúc mừng, app hỗ trợ RTL. Tuy nhiên không tin mù rằng mọi thứ đúng ngay. Kiểm tra support tốt như thế nào. Chọn RTL language làm primary device language.

Mẹo RTL:

  • Trong mọi layout, thay tất cả property Left và Right bằng Start và End tương đương. Ví dụ android:paddingLeft → android:paddingStart.
  • Nếu không có drawable đặc biệt RTL, mirror drawable hiện tại với attribute autoMirrored.
<vector xmlns:android="https://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:autoMirrored="true" android:tint="@color/colorForPreferenceIcon" android:viewportHeight="24.0" android:viewportWidth="24.0"> <path android:fillColor="#FF000000" android:pathData="M1.5,4V5.5C1.5,9.65 3.71,13.28 7,15.3V20H22V18C22,15.34 16.67,14 14,14C14,14 13.83,14 13.75,14C9,14 5,10 5,5.5V4M14,4A4,4 0 0,0 10,8A4,4 0 0,0 14,12A4,4 0 0,0 18,8A4,4 0 0,0 14,4Z" /> </vector>
  • Nếu có nơi cần LTR thay vì RTL, cũng làm được. Ví dụ force layout LTR: thêm android:layoutDirection="ltr" vào view. Force text direction trong TextView: android:textDirection="ltr".

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Khi dùng Translations Editor, có feature hữu ích: order professional translation service. Dù tôi không biết Hebrew, vẫn đặt mục tiêu add support Hebrew làm ví dụ RTL cho My Crypto Coins. Feature này nghe tốt, thử ngay. Order translation thành công English → Hebrew từ IDE với vài click.

Bắt đầu xây dựng Android App: Lập kế hoạch, Mockup, Thiết kế UI

Order a translation service

Final thoughts

Phần 2 series, tạo tất cả UI layout với code ban đầu dựa trên mockup chi tiết. Không muốn đi sâu chi tiết mỗi topic để không gây confused. Mục tiêu: thấy bức tranh toàn cảnh, focus vào detail nhỏ, tránh lỗi tốn kém sau.

Nếu thấy gì chưa cover đủ, dùng làm điểm nhảy để tự research. Luôn có resource tốt trên web để học. 😊

Repository

Tất cả XML layout và code phần này tại:

View Source On GitHub

Ačiū! Cảm ơn đã đọc! Bài gốc đăng trên blog www.baruckis.com ngày 14/5/2018.