คอร์สเรียน Android Application Developer

Android Developer 💚 Kotlin


ปิดปรับปรุงเนื้อหาจ้า


คอร์สนี้เหมาะกับ ?
  • นักศึกษา และ บุคคลทั่วไปที่สนใจภาษาโค้ด
  • เคยใช้ภาษาอื่นๆใดก็ได้ เช่น java ,php มาบ้างนิดนึง
  • คนที่สามารถติดตั้ง Android Studio เวอร์ชั่นล่าสุดพร้อมรัน Hello World ได้

หัวข้อที่เปิดสอน

Layout , View , Theme , Animation
  • ทำความรู้จักกับ Layout พื้นฐานที่ใช้กันบ่อยสุดๆใน Android App
  • ลองสร้าง Register Layout กันเลย ( เราจะลองใช้ Material Design ในหน้านี้กันด้วยครับ )  
  • การใส่ Style ให้ View แต่ละชิ้นใน Layout 
  • การใส่ Animation ต่างๆให้กับ View
  • การใส่ Animation ในจังหวะปิดเปิด Activity
  • Constraint Layout ลากวาง View บน Layout อย่างอิสระ 
  • Animation พิเศษของ Constraint Layout (ทำเกมส์เล็กๆ ให้เพื่อนเล่นได้เลยนะ)
  • Support Tablet Screen มาลองแยก Layout สำหรับ Tablet และหน้าจอใหญ่ๆกัน

Container of View 
  • มาลองสร้าง Custom ViewPager (การเปิดหน้าใหม่ โดยใช้การเลื่อนที่ละหน้า)
  • เปลี่ยน ViewPager ธรรมดา ให้เป็นเหมือนการเปิด Book , Zoom Out , Fade Out และอื่นๆ)
  • มาลองสร้าง RecyclerView แบบ Grid , List

Room (Offline database for android)
  • การเพิ่มข้อมูลลงใน database
  • การดึงข้อมูลแบบต่างๆ จาก databade
  • การอัพเดทข้อมูลใน database
  • การลบข้อมูลที่อยู่ใน databade

______________________________________________

ระยะเวลาการสอน
  • วันเสาร์ - อาทิตย์ ช่วงเวลา 9.00 - 16.00น.

สถานที่เรียน
  • สำหรับเพื่อนๆที่อยู่ กทม. สามารถนัดเรียนได้ที่เดอะมอลบางแค (BTS สถานีบางแค) 
  • เพื่อนๆที่อยู่ต่างจังหวัด สามารถเดินทางไปสอนได้ คิดค่าเดินทางไม่แพงจ้า

สิ่งที่ผู้เรียนจะได้รับ
นอกเหนือจากหัวข้อคอร์สเรียน
  • การหาความรู้เรื่อง Android App ด้วยตัวเอง
  • ย่นระยะเวลาการเรียนรู้เพราะการเรียนรู้ที่เร็ว คือการมีคนสอนนี่แหละ (มีเวลาไปทำอย่างอื่น)
  • ได้สร้าง Android App เกมส์ 1 เกมส์สำหรับฝึกสมาธิได้ด้วยนะครับ >_<
  • เทคนิค การสร้างรายได้จากแอพ Android app (อย่างถูกกฏหมายนะครับ)
  • ได้ลอง สร้าง Icon Android App บน Android Studio
  • การนำแอพขึ้น Google PlayStore
  • การรับเงินจากแอปที่ขายได้
  • การอัพเดทเวอร์ชั่นแอปบน Google PlayStore
  • ท่องไปใน Google Play Console บ้านหลังที่ 2 ที่มีของดีดีมากมายสำหรับ Android Developer

ราคาคอร์สเรียนทั้งหมดนี้
  • ราคา 3,600 THB.-

เพื่อนๆจะได้ลองทำแอพจริงๆ 
แบบ Step by Step เรียนกันแบบชิลๆ
สอนตัวต่อตัว หรือมากับเพื่อน
(จำกัดรอบละไม่เกิน 3ท่าน)
เพื่อความสะดวกในการสอนครับ :)

(กรณีผู้เรียนเป็นมือใหม่จริงๆ)
เราอธิบายเป็นภาษาทั่วไปง่ายๆ 
ให้เข้าใจได้นะ ;)

สอบถาม/ลงทะเบียนเรียน




Read more ...

แนวทางของการเขียน android app ให้ปลอดภัย - Android App security best practices [2020]


Enforce secure communication

Use implicit intents and non-exported content providers


Show an app chooser

ถ้า implicit intent สามารถเรียกเปิด แอพที่2
บนอุปกรณ์ของผู้ใช้
ให้ทำ app chooser วิธีนี้ช่วยให้ผู้ใช้โอนถ่าย sensitive information ไปยัง appที่เขาไว้วางใจ


val intent = Intent(ACTION_SEND)
val possibleActivitiesList: List<ResolveInfo> = queryIntentActivities(intent, PackageManager.MATCH_ALL)


// Verify that an activity in at least two apps on the user's device
// can handle the intent. Otherwise, start the intent only if an app
// on the user's device can handle the intent.


if (possibleActivitiesList.size > 1) {

    // Create intent to show chooser.
    // Title is something similar to "Share this photo with".

    val chooser = resources.getString(R.string.chooser_title).let { title ->
        Intent.createChooser(intent, title)
    }
    startActivity(chooser)
} else if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}





Apply signature-based permissions


<permission    
   android:name="com.example.ak1.PREM"    
   android:protectionLevel="signature" />  // กำหนดให้สามารถเข้าถึงได้เฉพาะ App ที่สร้างโดย Keystore เดียวกัน

เพิ่มเติม : android:protectionLevel


ตัวอย่างการนำมาใช้กับ broadcast receiver

<receiver    
  android:name=".OnWifiEnabled"    
  android:permission="com.example.ak1.PERM">    
     <intent-filter>        
       <action android:name="android.net.wifi.STATE_CHANGE" />    
     </intent-filter>
</receiver>


อันตรายจากการไม่ใช้  Custom Permission
https://iak1.blogspot.com/2015/09/android-intent-spoofing.html



Disallow access to your app's content providers

ถ้าคุณตั้งใจจะส่งข้อมูลจากแอพคุณไปยังแอพอื่น (ที่ไม่ใช่แอพของคุณ)
ไม่ควรอนุญาติให้ app ตัวอื่นเข้าถึง contentProvider ที่แอพคุณมี
หากแอพของคุณสามารถติดตั้งบน android 4.1.1 (api level 16)
หรือต่ำกว่านั้น android:exported attribute จะเป็น "true" by default

<provider
  android:name="android.support.v4.content.FileProvider"
  android:authorities="com.example.myapp.fileprovider"
       ...
  android:exported="false">
  <!-- Place child elements of <provider> here. -->
</provider>

แล้วถ้าไม่ประกาศ exported=false ก็จะเจอกับ Exploiting Content Provider 
(SQL Injection)




Ask for credentials before showing sensitive information

เมื่อขอข้อมูลประจำตัวจากผู้ใช้เพื่อให้สามารถเข้าถึง sensitive information หรือ premium content ในแอพ ให้ขออย่างใดอย่างนึงเช่น Pin / Password / Pattern หรือ biometric credential เช่น fingerprint

ตัวอย่างการทำ Biometric Authen ทั้งแบบ kotlin และ javaได้ที่ https://developer.android.com/topic/security/best-practices#ask-for-credentials


System dialog requesting biometric authentication



Apply network security measures


Use SSL traffic

ถ้า app ของคุณมีการติดต่อกับ web server ที่มี certificateที่ออกโดย บริษัทที่มีชื่อเสียงและเชื่อถือได้
ใช้ https request แบบ simple simple แบบนี้

val url = URL("https://www.google.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
    ...
}



Add a network security configuration

ถ้า app ของคุณใช้ custom CAs
ก็ยังสามารถทำการตั้งค่า network's security settings ได้ใน configuration file

วิธีการสร้าง configuration โดยไม่ต้องแก้ไขโค้ดในแอพ

1. Declare the configuration in your app's manifest:

<manifest ... >
    <application
        android:networkSecurityConfig="@xml/network_security_config"
        ... >
        <!-- Place child elements of <application> element here. -->
    </application>
</manifest>


2.เพิ่ม XML resource file ไว้ใน res/xml/network_security_config.xml
ระบุว่าให้ traffic ทั้งหมดอยู่บนโดเมนเฉพาะ ควรใช้ HTTPS มาปิดการใช้งาน clear-text


<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">secure.example.com</domain>
        ...
    </domain-config>
</network-security-config>


3.ระบุใน network security configuration XML file:

<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

อ่านแบบละเอียดๆ - Network security configuration
https://developer.android.com/training/articles/security-config.html



Create your own trust manager

SSL ของคุณไม่ควรยอมรับทุกใบรับรอง
คุณอาจต้องการ setup trust manager และ handle ssl warning ที่เกิดขึ้น
หากมีเงื่อนไขใดๆต่อไปนี้ใน use case

- คุณกำลังติดต่อกับ web server ที่มี certificate ที่ signed โดย custom CA
- CA ที่ไม่น่าเชื่อถือจาก device ที่คุณกำลังใช้
- คุณไม่สามารถใช้ network security configuration.

To learn more about how to complete these steps, see discussion about handling an : unknown cerificate authority.

ข้อมูลที่เกี่ยวข้อง :





Provide the right permissions


Use intents to defer permissions

เป็นไปได้คุณไม่ควรเพิ่ม permission
เพื่อดำเนินการอะไรบางอย่างให้เสร็จสมบูรณ์ในแอพอื่น
ใช้ intent เพื่อ defer request ไปยังแอพที่ต่างกันที่มี permission อยู่แล้ว

ตัวอย่างการใช้ intent เพื่อพาผู้ใช้ไปยัง Contacts app
แทนการร้องขอ READ_CONTACTS and WRITE_CONTACTS permissions:

// Delegates the responsibility of creating the contact to a contacts app,
// which has already been granted the appropriate WRITE_CONTACTS permission.

Intent(Intent.ACTION_INSERT).apply {
    type = ContactsContract.Contacts.CONTENT_TYPE
}.also { intent ->
    // Make sure that the user has a contacts app installed on their device.
    intent.resolveActivity(packageManager)?.run {
        startActivity(intent)
    }
}


นอกจากนี้, ถ้า app ของคุณต้องการดำเนินการ file-based I/O เช่นการเข้าถึงที่เก็บข้อมูล
หรือการเลือกไฟล์ต่างๆ, มันไม่ต้องการ Special permissions เพราะว่าระบบสามารถดำเนินการให้เสร็จสมบูรณ์อยู่แล้ว


Share data securely across apps

  • การ share app content กับแอพอื่นๆในแบบที่ปลอดภัย 
  • ใช้ read-only หรือ write-only permissions ตามความจำเป็น
  • จัดการ one-time access to data โดยใช้ FLAG_GRANT_READ_URI_PERMISSION และ FLAG_GRANT_WRITE_URI_PERMISSION flags.



Store data safely

Store private data within internal storage

เก็บข้อมูลส่วนตัวของผู้ใช้ทั้งหมดไว้ใน
devicei internalstorage ที่มี sanboxed แต่ละ app.

แอพของคุณไม่ต้องการ request permission เพื่อดูไฟล์เหล่านี้, และแอพอื่นๆไม่สามารถเข้าถึงไฟล์ เป็นการรักษาความปลอดภัยที่เพิ่มเข้ามา

เมื่อ user uninstall app ... device จะลบทุกไฟล์ที่แอพเซฟไว้ใน internal storage

// Creates a file with this name, or replaces an existing file that has the same name. 
// Note that the file name cannot contain path separators.

val FILE_NAME = "sensitive_info.txt"
val fileContents = "This is some top-secret information!"

openFileOutput(FILE_NAME, Context.MODE_PRIVATE).use { fos ->
    fos.write(fileContents.toByteArray())
}


Use external storage cautiously

โดย default, android system ไม่บังคับใช้ข้อจำกัดด้านความปลอดภัยบนข้อมูลที่อยู่ใน external storage, และการจัดเก็บข้อมูลไม่รับประกัณว่าจะสามารถเชื่อมต่อกับอุปกรณ์ได้ (External storage directory แต่ละเครื่องไม่เหมือนกัน), คุณควรใช้มาตรการความปลอดภัยต่อไปนี้ เพื่อให้การเข้าถึงข้อมูลอย่างปลอดภัยใน external storage

        > Use scoped directory access
ถ้า app ของคุณต้องการ เข้าถึงเฉพาะ directory ที่กำหนดใน external storage ของอุปกรณ์,
คุณสามารถใช้ scoped directory access เพื่อจำกัด app ที่จะเข้าถึงที่เก็บข้อมูล external storage ของ device ....เพื่อความสะดวกแก่ผู้ใช้ app คุณควร save directory access URI เพื่อให้ผู้ใช้ไม่จำเป็นต้องอนุมัติการเข้าถึง directory ทุกครั้งที่แอพของคุณพยาม access มัน

Note* ถ้าคุณใช้ scoped directory access กับ directory ที่เฉพาะเจาะจงใน external storage, ผู้ใช้อาจจะลบกันได้ดังนั้นควรจะมี logic ที่มาควบคุมโดยใช้ Environment.getExternalStorageState()
จะ return value ที่เป็น พฤติกรรมของผู้ใช้

The following code snippet uses scoped directory access with the pictures directory
within a device's primary shared storage:


private const val PICTURES_DIR_ACCESS_REQUEST_CODE = 42

...

private fun accessExternalPicturesDirectory() {
    val intent: Intent = (getSystemService(Context.STORAGE_SERVICE) as StorageManager)
            .primaryStorageVolume.createAccessIntent(Environment.DIRECTORY_PICTURES)
    startActivityForResult(intent, PICTURES_DIR_ACCESS_REQUEST_CODE)
}

...

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == PICTURES_DIR_ACCESS_REQUEST_CODE && resultCode == Activity.RESULT_OK) {

        // User approved access to scoped directory.
        if (resultData != null) {
            val picturesDirUri: Uri = resultData.data

            // Save user's approval for accessing this directory
            // in your app.
            contentResolver.takePersistableUriPermission(
                    picturesDirUri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION
            )
        }
    }
}

คำเตือน !! อย่าส่งค่า null เข้าไปใน createAccessIntent() เกินความจำเป็นเพราะนี้จะทำให้แอปของคุณเข้าถึงปริมาณทั้งหมดที่ StorageManager ค้นหาแอปของคุณ



      > Check validity of data

ถ้าแอปของคุณใช้ข้อมูลจาก external storage, ต้อง make sure ว่า contents ของข้อมูล
ไม่ได้รับความเสียหาย หรือ modified แอปของคุณควรจะมี logic เพื่อจัดการความถูกต้องด้วย

The following example shows the permission and logic that check a file's validity:

<manifest ... >

    <!-- Apps on devices running Android 4.4 (API level 19) or higher cannot
         access external storage outside their own "sandboxed" directory, so
         the READ_EXTERNAL_STORAGE (and WRITE_EXTERNAL_STORAGE) permissions
         aren't necessary. -->

    <uses-permission
          android:name="android.permission.READ_EXTERNAL_STORAGE"
          android:maxSdkVersion="18" />
    ...
</manifest>



MyFileValidityChecker

private val UNAVAILABLE_STORAGE_STATES: Set<String> =
        setOf(MEDIA_REMOVED, MEDIA_UNMOUNTED, MEDIA_BAD_REMOVAL, MEDIA_UNMOUNTABLE)
...
val ringtone = File(getExternalFilesDir(DIRECTORY_RINGTONES), "my_awesome_new_ringtone.m4a")
when {
    isExternalStorageEmulated(ringtone) -> {
        Log.e(TAG, "External storage is not present")
    }
    UNAVAILABLE_STORAGE_STATES.contains(getExternalStorageState(ringtone)) -> {
        Log.e(TAG, "External storage is not available")
    }
    else -> {
        val fis = FileInputStream(ringtone)

        // available() determines the approximate number of bytes that
        // can be read without blocking.

        val bytesAvailable: Int = fis.available()
        val fileBuffer = ByteArray(bytesAvailable)
        StringBuilder(bytesAvailable).apply {
            while (fis.read(fileBuffer) != -1) {
                append(fileBuffer)
            }

            // Implement appropriate logic for checking a file's validity.

            checkFileValidity(this)
        }
    }
}



Store only non-sensitive data in cache files

เพื่อให้เข้าถึงได้เร็วขึ้น non-sensitive app data ควรเก็บมันไว้ใน device's cache.
สำหรับ cache ที่ใหญ่กว่า 1MB ใช้ getExternalCacheDir() หรือใช้ getCacheDir()


The following code snippet shows how to cache a file that your app recently downloaded:

val cacheFile = File(myDownloadedFileUri).let { fileToCache ->
    File(cacheDir.path, fileToCache.name)
}



Use SharedPreferences in private mode

อันนี้คือ basic อยู่แล้วขอข้ามคำอธิบายไป แต่แนะนำว่า
ถ้าต้องการแชร์ data ข้าม app ห้ามใช้ SharedPreferences
ให้ย้อนไปอ่าน share data securely across apps.

เพิ่มเติม : http://trymydroid.blogspot.com/2017/06/hack-android-hack-sharedpreference.html

Update*
https://trymydroid.blogspot.com/2020/01/encryptedsharedpreferences-security.html



Keep services and dependencies up-to-date


Check the Google Play services security provider
section นี้ใช้กับ app ที่มีการใช้งาน google play service เท่านั้น


Update all app dependencies
ก่อนการ deploy app ต้อง make sure ก่อนว่า  libraries, SDKs, และ dependencies อื่นๆ อัพเดทเป็นเวอร์ชั่นล่าสุด

ตัวอย่างเช่น Android SDK ใช้ tool บน Android Studio อย่าง SDK Manager ที่มา update ให้
หรืออีกตัวอย่าง third-party dependencies ให้เช็คที่ website ของ libraries ที่ app เพื่ออัพเดท security patches



...........................SUMMARY............................


-  Signature-basedpermissions

- Disallow access to your app's content providers

- ทำ app chooser ช่วยให้ผู้ใช้สามารถเลือกโอนถ่าย sensitive information  ไปยัง appที่เขาไว้วางใจ

- เพื่อให้สามารถเข้าถึง sensitive information หรือ premium content ในแอพ ให้สอบถาม
Password / Pattern หรือ biometric credential เช่น fingerprint


- Use SSL traffic

  • ถ้า app ของคุณมีการติดต่อกับ web server ที่มี certificateที่ออกโดย บริษัทที่มีชื่อเสียงและเชื่อถือได้ 
  • ถ้า app ของคุณใช้ custom CAsก็ยังสามารถทำการตั้งค่า network's security settings ได้ใน configuration file

การใช้ intent เพื่อพาผู้ใช้ไปยัง Contacts app แทน READ_CONTACTS and WRITE_CONTACTS permissions

  • การ share app content กับแอพอื่นๆในแบบที่ปลอดภัย 
  • ใช้ read-only หรือ write-only permissions ตามความจำเป็น
  • จัดการ one-time access to data โดยใช้ FLAG_GRANT_READ_URI_PERMISSION และ FLAG_GRANT_WRITE_URI_PERMISSION flags.

- เก็บข้อมูลส่วนตัวของผู้ใช้ทั้งหมดไว้ใน device internal storage ที่มี sanboxed แต่ละ app.

  • การเข้าถึงข้อมูลอย่างปลอดภัยใน external storage
  • Use scoped directory access
  • Environment.getExternalStorageState() จะ return value ที่เป็น พฤติกรรมของผู้ใช้

- ถ้าแอปของคุณใช้ข้อมูลจาก external storage,
ต้อง make sure ว่า contents ของข้อมูลไม่ได้รับความเสียหาย หรือ modified แอปของคุณควรจะมี logic เพื่อจัดการความถูกต้องด้วย

- non-sensitive app data ควรเก็บมันไว้ใน device's cache. สำหรับ cache ที่ใหญ่กว่า 1MB ใช้ getExternalCacheDir() หรือใช้ getCacheDir()

  • Check the Google Play services security provider
  • Update all app dependencies


Ref: https://developer.android.com/topic/security/best-practices

Read more ...

Android - 2 Type of CustomView

ถ้ามั่นใจว่าไม่มี Library ที่ตอบโจทย์ หรืออยากจะสร้าง View ตัวนึงไว้แสดงใน Layout 

( และช่วยป้องกันไฟล์ View XML ของเรา จากการ Decompile ให้แกะยากขึ้นไปอีก ) ก็มีทางเลือกก็คือการทำ CustomView นี้เอาเองนี้แลครับ 


การสร้าง Custom UI Component 2 วิธี

วิธีแรก - ใช้วิธีสืบทอดจากคลาสแม่เช่น
EditText , DialogFragment , Button, TextView , RecyclerView
CheckBox,  RadioButton, Gallery, Spinner บลาๆ 
เราไม่จำเป็นต้องวาดมุมมองทั้งหมด
และเราสามารถ override method จากคลาสแม่เพื่อ customize view ต่อได้ (อยู่ล่างๆนะ)

วิธีที่สอง - สืบทอดจากคลาสชื่อ "View" โดยตรง
ต้องใช้ onDraw() เพื่อวาด View ใหม่ทั้งหมด

เพิ่มเติม Drawing an extended View



เริ่มที่ วิธีแรก - Customizing a View subclass

เราสามารถใช้คลาสที่จะสร้างนี้เพื่อกำหนด appearance (รูปทรง) และ attributes (คุณสมบัติ)
โดยเราสามารถ override methods ที่มีจากคลาสแม่ 
(ใครอ่านแล้วงง แนะนำศึกษาเรื่อง OOP ก่อนนะครับ) 



จากตัวอย่างโค้ดด้านล่าง (โค้ดเก่า ดูพอเป็นตัวอย่างนะครับ)
เช่น AppCompatEditText  มีคลาส TextView เป็นคลาสแม่


ดูได้จากในนี้ : https://developer.android.com/reference/android/support/v7/widget/AppCompatEditText

เราสามารถย้อนกลับไปใช้ setCompoundDrawablesRelativeWithIntrinsicBounds 

ซึ่งเป็น Public Method ของ TextView ดูได้จาก


class EditTextWithClear : AppCompatEditText {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if(text?.length == 1){

            val mClearButtonImage = ResourcesCompat.getDrawable(resources, R.drawable.ic_cancel, null)

            setCompoundDrawablesRelativeWithIntrinsicBounds(
                null,          // Start of text.
                null,          // Above text.
                mClearButtonImage// End of text.
                null        // below text.
            )
        }
        return super.onKeyDown(keyCode, event)
    }
}


วิธีนำไปใช้ใน Layout file

<com.example.trymycustomview.EditTextWithClear
      android:maxLength="6"
      android:layout_width="300dp"
      android:layout_height="wrap_content"
      android:id="@+id/edtWithClear"/>



Ref : https://google-developer-training.github.io/android-developer-advanced-course-concepts/unit-5-advanced-graphics-and-views/lesson-10-custom-views/10-1-c-custom-views/10-1-c-custom-views.html#customizing-subclass
Read more ...

Data and file storage on Android

img src : https://developer.android.com/training/data-storage

Preferences :  *ไฟล์หายเมื่อลบแอพ
 - ข้อมูลที่เก็บแบบมี key กับ value ในไฟล์ .xml (เจอบ่อยสุด)
 - Shared Preference
 - Secure Preference
 - EncryptedSharedPreferences

Database : ฐานข้อมูล ใน android app *ไฟล์หายเมื่อลบแอพ
  - SQLiteOpenHelper
  - Room
  - SQLCipher

Shared storage : ข้อมูลที่แชร์กับแอปอื่นๆ
 เช่น Media : images , audio , videos , documents *ไฟล์ไม่หายเมื่อลบแอพ
 - MediaStore API
 - Content Provider สำหรับเปิด Database ให้แอพอื่นสามารถ Query มาที่แอพเราได้
                                สามารถกำหนดได้เช่น เฉพาะแอพที่ใช้ keystore เดียวกัน

App-specific storage : แบ่งออกเป็น 2 ประเภท

Internal Storage (เก็บน้อยๆแต่ปลอดภัย) *ไฟล์หายเมื่อลบแอพ
 - getFilesDir()  ไฟล์จะถูกเก็บที่ : package_name/files/
 - getCacheDir() ไฟล์จะถูกเก็บที่ : package_name/cache/

External Storage (เก็บข้อมูลเยอะๆ) *ไม่ลบไฟล์เมื่อลบแอพ
 - getExternalFilesDir() ไฟล์จะถูกเก็บที่ : /storage/sdcard0/Android/data/package_name/files/........
 - getExternalCacheDir()  ไฟล์จะถูกเก็บที่ : ..........................................

Read more ...

EncryptedSharedPreferences (Security library, part of Android Jetpack)

build.gradle

setup :
- minSdkVersion 23
- implementation "androidx.security:security-crypto:1.0.0-alpha02"


MainActivity.kt

onCreate()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
    // Create MasterKey for encryption / decryption
    val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

    // Create EncryptedSharedPreferences file name : account_ESP and put password.
    val encryptedSharedPreferences = EncryptedSharedPreferences.create(
        "account_ESP",
        masterKey,
        applicationContext,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)
    val editor = encryptedSharedPreferences.edit()
    editor.putString("password", "123456789")
    editor.apply()

    // Get password (result == plaintext)
    val password = encryptedSharedPreferences.getString("password", null)
    Log.e("TEST:password_ESP:", password.toString())

}else{

    // Create Original SharedPreference File and put password.
    val sharePreference = this.getSharedPreferences("account_SP", Context.MODE_PRIVATE)
    val editor = sharePreference.edit()
    editor.putString("password", "123456789")
    editor.apply()

    val password = sharePreference.getString("password", null)
    Log.e("TEST:password_SP:", password.toString())
}

_______________________________________________________

output : /data/data/package_name/shared_prefs/account_ESP.xml

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="AX88cKNiR6uVSVZdHxuE8pClSn+eRImmQrYUdDo=">AVQvIUN1bCzJsADFgb01UHlN8R7Ce23e+odonhBGXBWKLjRq7oDgSh6X/DT1J1yyo1g=</string>
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">12880177601ca5032985229fea7d3bdb61a0345d66db7393e61e29fb92d84bec09f1c1403d3d2b2b63952ab135aeea56ae2525d9916ca18f4717d88ee28e2c8190320e82d82435ba19c03817be63b1413d683d24c03a9c0fc02e6b2354d07f28c9e908709e5648482754130a57c5fcf9093bad099255cb330436c444c494da65d91acd7e15848e3b69932d1a4408c3c2bca105123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118c3c2bca1052001</string>
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a901e0af0b350f762ac68f7c0676d52922e861735966b9d2667ba964b859a3c0ad3d1c45127a4870c19aafa66423e2a4ee34f136ad21e6c8b6fbbe2cf6923675b996e1302f616d58f8c54829c90acc3455fada06c3380991571a5eadc3d865d90403c73bf7f7c53acebbad725d572d2ab8b02f0723238406b961f573f31abe555db4cdab66e7c1b28fd76d002d80aa8070cdd3ad36c409adb8335047578f75bd8749e87752e30c3cb5a0d81a4408a3e1f1f907123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579100118a3e1f1f9072001</string>
</map>


Ref : https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences
Read more ...

พื้นฐาน android - การใช้ apply ในภาษา kotlin (เทียบกับการเขียนแบบเดิม)

ในตัวอย่างนี้คือการทำ Fade animation  (แบบ Hardcode) ให้ imageView
ตัวอย่างการเขียนแบบเดิมในสไตค์ของ Java ที่เราคุ้นเคย

[Java Style]
imageView.alpha = 0f
imageView.animate()
       .alpha(1f)
       .setDuration(longAnimationDuration.toLong())
       .setListener(null)

(งานจริงส่วนใหญ่เรื่อง animation จะทำเป็น xml ไฟล์เก็บไว้ใน /res/anim/.... )

______________________________________________________

พอมามองในสไตค์ของ kotlin มี apply เพิ่มมาให้ใช้
มันจะ return "this" ก็คือ object view ของตัวมันเอง เข้ามาในปีกกา {}
เหมือนจะบอกว่า ถ้าจะทำอะไรฉัน ก็ทำได้แค่ในนี้นะ...
กำจัดสโคปของตัวแปร + โค้ดดูดีมีระเบียบไปอีกแบบดีครับ

[Kotlin Style]
imageView.apply {
    this.alpha = 0f
    this.animate()
        .alpha(1f)
        .setDuration(longAnimationDuration.toLong())
        .setListener(null)
}


Usage

imageView.apply {
     // เราพิมพ์ it.  ตรงนี้
}

แอนดรอยสเตอร์ดิโอจะบอกเราเองว่าสามารถทำอะไรต่อๆไปได้บ้าง




Read more ...

รวมลิงค์เว็บของ Free Free ที่อยากส่งต่อ [ Image , Sound , Fonts , Icon ]

Read more ...

การวางโครงสร้าง Android App แบบ MVP (Register Example)

บทความนี้ไม่เน้นความสวยงามเน้อ เน้นความเข้าใจโครงสร้างและโฟว์การทำงานของ MVP ครับ #For dev


ติดตั้ง dependencies

build.gradle (Module : app)

// RETROFIT ...  NETWORK LIBRARY
implementation 'com.squareup.retrofit2:retrofit:2.4.0'

// RETROFIT .. CONVERTER & ADAPTER
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'

// RX-ANDROID

implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'



วาง Layout แบบนู๊ปๆ >_<

activity_main.xml  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edtUsername"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="username :"
        android:inputType="text" />

    <EditText
        android:id="@+id/edtPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="password : "
        android:inputType="textPassword" />

    <Button
        android:id="@+id/btnRegister"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Register" />
</LinearLayout>



MainActivity.kt

class MainActivity : AppCompatActivity(), MainActivityPresenter.View {

    private var presenter: MainActivityPresenter? = null
    private var progressBar: ProgressBar? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        presenter = MainActivityPresenter(this)

        val edtUsername = findViewById<EditText>(R.id.edtUsername)
        val edtPassword = findViewById<EditText>(R.id.edtPassword)

        // setup Progress bar
        initProgressBar()

        // Update ตัวแปร username , password แบบทีละตัวอักษร

        edtUsername.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(char: CharSequence, start: Int, before: Int, count: Int) {
                presenter!!.updateUsername(char.toString())   // เรียกใช้ method updateUsername ใน presenter
            }

            override fun afterTextChanged(s: Editable) {
                hideProgressBar()
            }
        })

        edtPassword.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(char: CharSequence, start: Int, before: Int, count: Int) {
                presenter!!.updatePassword(char.toString())  // เรียกใช้ method updatePassword ใน presenter
            }

            override fun afterTextChanged(s: Editable) {
                hideProgressBar()
            }
        })

        // คลิก Register
        btnRegister.setOnClickListener {
            presenter!!.submitRegister()   // เรียกใช้ method submitRegisterใน presenter
        }
    }

    private fun initProgressBar() {
        progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleSmall)
        progressBar!!.isIndeterminate = true
        val params = RelativeLayout.LayoutParams(Resources.getSystem().displayMetrics.widthPixels, 250)
        params.addRule(RelativeLayout.CENTER_IN_PARENT)
        this.addContentView(progressBar, params)
        showProgressBar()
    }

    // รอเรียกใช้จากฝั่ง presenter
    override fun showProgressBar() {
        progressBar!!.visibility = View.VISIBLE
    }

    override fun hideProgressBar() {
        progressBar!!.visibility = View.INVISIBLE
    }

}


MainActivityPresenter.kt

class MainActivityPresenter(private val view: View) {

    private val user: User = User()  // แยก Model User สำหรับการทำงานในโมดูล User

    fun updateUsername(username: String) {
        user.username= username
    }

    fun updatPassword(password: String) {
        user.password= password

    }

    fun submitRegister(){
       val response = user.register()     << Response โผล่นี่
        ..
        ..
        // สั่งอัพเดท View อะไร บลาๆ แล้วแต่ชอบเลยครับ
    }


    // ตัวอย่างการใช้ Interface ส่งข้อมูลกลับไปที่ MainActivity.kt
    interface View {
        fun showProgressBar()
        fun hideProgressBar()
    }
}



User.kt

class User {

    var username = ""
    var password = ""


    fun getUserProfile(): String {
        return "Username: $usrename\n Password : $password"
    }

    fun register() : Boolean{
        val post = PostWithBody(username, password) // Format ก้อนข้อมูลที่จะส่งออก
        val retrofit = Retrofit.Builder().addConverterFactory(
            GsonConverterFactory.create(GsonBuilder().create()))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl("https://jsonplaceholder.typicode.com/").build()

        val userApi = retrofit.create(UserApi::class.java)
        val response = userApi.registerMember(post)

        response.observeOn(AndroidSchedulers.mainThread()).subscribeOn(IoScheduler()).subscribe {

            Log.e("TEST:", it.toString())
            ..
            ..
            ..
            // ไปต่อกันเองเน้อ
        }
        return true
    }

    ..
    ..
    ..
    // ตรงนี้อาจจะมีฟังก์ชั่น Login() มา เพิ่มเติมได้นะ

}



Interface ของ Retrofit
UserApi.kt


interface UserApi {

    @POST("/posts") // ส่งข้อมูลแบบ Post Request ไปที่ https://jsonplaceholder.typicode.com/posts

    fun registerMember(@Body data: PostWithBody): Observable<RegisterResponse // ข้อมูลที่ส่งออก , ข้อมูลที่รับเข้า

}


Format ของ Object ที่ใช้ รับ, ส่งกับ Server
ถ้าทำเป็นทีมก็ ตะโกนถาม Backend  เอ้ยๆ อันนี้ๆๆๆๆ ปะวะ ( แล้วก็ตั้งผิดอยู่ดี >_<" )

PostWithBody.kt

data class PostWithBody(
    @SerializedName("username") val username: String,  // ทำไมต้องใส่ SerializedName 
    @SerializedName("password") val password: String
)

RegisterResponse.kt

data class RegisterResponse(
    @SerializedName("response") val response: String
     ..
     ..
)

Read more ...