[Training] Có bao nhiêu cách để start một activity bằng Kotlin Android

Theo bạn thì có bao nhiêu cách để start một activity trong android? Nếu bạn là một lập trình viên Android giống như tôi, thì tôi tin chắc bạn sẽ trả lời ngoài cách new intent ra thì làm gì còn cách nữa. Điều đó chỉ đúng với ngôn ngữ java-android thôi còn với Kotlin thì sao, điều này có còn đúng? Tôi cũng khá bất ngờ khi thấy câu hỏi này trên medium.com, và cũng google thử với keyword "How many ways to start a new activity in android" Với một danh sách dài dằng dặc câu trả lời cho nó, tôi tìm thấy một bài viết khá thú vị và muốn chia sẻ cho các bạn

Có bao nhiêu cách để start activity Kotlin

Bài viết được biên dịch từ nguồn https://medium.com/@passsy/starting-activities-with-kotlin-my-journey-8b7307f1e460

1. newIntent() in Java

Phương thức newIntent được dùng với các activity để tạo ra 1 thể hiện của nó, bạn thường thấy khi bạn tạo 1 Fragment mới thì hệ thống sẽ tự generate cho ta 1 phương thức tên là newInstance (tạo ra thể hiện mới cho fragment đó), thì newIntent cũng tương tự vậy. Mặc dù phương thức này không được giới thiệu trong các văn bản chính thống nhưng lại được người ta sử dụng rộng rãi.

Và đây nó sẽ như thế này

public class UserDetailActivity extends Activity {

    private static final String INTENT_USER_ID = "user_id";

    public static Intent newIntent(final Context context, final User user) {
        final Intent intent = new Intent(context, UserDetailActivity.class);
        intent.putExtra(INTENT_USER_ID, user.getId());
        return intent;
    }

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final String userId = getIntent().getStringExtra(INTENT_USER_ID);
        if (userId == null) {
            throw new IllegalStateException("field " + INTENT_USER_ID + " missing in Intent");
        }
    }
}
public class OtherActivity extends Activity {

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //...
        mUserList.setOnUserClickListener(User user -> {
            final Intent intent = UserDetailActivity.newIntent(this, user);
            startActivity(intent);
        })
    }
}

Intent chỉ được tạo ra trong class UserDetailActivity chứ không phải bên ngoài. Nếu bạn không làm theo cách này có thể bạn sẽ quên mất một giá trị extra nào đó mà bạn mang theo trong intent. Và cũng chú ý rằng nếu làm theo cách này thì các giá trị constant cũng có thể định nghĩa là private được nhé. Nào chúng ta cùng biên dịch đoạn code trên ra ngôn ngữ Kotlin nhé, và chúng ta sẽ cải tiến nó nếu cần thiết ở phần sau

class UserDetailActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val userId = intent.getStringExtra(INTENT_USER_ID)
                ?: throw IllegalStateException("field $INTENT_USER_ID missing in Intent")
    }

    companion object {

        private val INTENT_USER_ID = "user_id"

        fun newIntent(context: Context, user: User): Intent {
            val intent = Intent(context, UserDetailActivity::class.java)
            intent.putExtra(INTENT_USER_ID, user.id)
            return intent
        }
    }
}
class OtherActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //...
        userlist.clickListener { user ->
            val intent = UserDetailActivity.newIntent(this, user)
            startActivity(intent)
        }
    }
}

Vì Kotlin không có giá trị static mà thay vào đó chúng ta có đối tượng companion thay thế, nó là 1 đối tượng được chia sẻ giữa các thể hiện của lớp chứa nó.

2. Generic launchActivity() extension function

Để cố gắng trở thành 1 ngôn ngữ thông minh, Kotlin đã mang lại cho chúng ta nhiều khả năng tùy biến code hơn, và tôi ngay lập tức đã tạo ra 1 extension method để launch Activity khác. Và tôi sẽ chỉ cho bạn thấy nó hoạt động như thế nào

const val INTENT_USER_ID = "user_id"

class UserDetailActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val userId = intent.getStringExtra(INTENT_USER_ID)
        requireNotNull(userId) { "no user_id provided in Intent extras" }
    }
}
class OtherActivity: Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //...
        userList.clickListener { user ->

            // super simple, no arguments for simple Activities but doesn't prevent a crash for this one
            launchActivity<UserDetailActivity>()

            // add the required argument
            launchActivity<UserDetailActivity> {
                putExtra(INTENT_USER_ID, user.id)
            }

            // add custom flags
            launchActivity<UserDetailActivity> {
                putExtra(INTENT_USER_ID, user.id)
                addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
            }

            // use options for cool animations
            val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatar, "avatar")
            launchActivity<UserDetailActivity>(options = options) {
                putExtra(INTENT_USER_ID, user.id)
            }
            
            // requestCode, why not!?
            launchActivity<UserDetailActivity>(requestCode = 1234) {
                putExtra(INTENT_USER_ID, user.id)
            }
        }
    }
}

Điều này thật tuyệt phải không, và bạn sẽ dễ dàng nhận thấy một số ưu điểm của phương thức này:

  1. ít code, flexibility hơn các start Activity thông thường
  2. 1 method dùng cho mọi trường hợp và bạn ko cần quan tâm đến cả launchActivityForResult nữa

Nhưng có điểm mà tôi không thích với ngôn ngữ này đó là các giá key extra bị public, và điều này dẫn đến việc nó không bảo mật bằng java ( dễ nhận thấy khi bạn viết library mà bạn không muốn public ra ngoài) Tuy nhiên thì nó cũng ngon hơn startActivity() nhiều

3. Moving the launch function to the Activity file

Sau khi viết phương thức trên tôi bắt đầu nhận thấy nó trông khá tệ, tôi đã nghĩ đến cách implement và override phương thức này cho tất cả các activity mà tôi sẽ sử dụng

@JvmOverloads
fun Context.launchUserDetailActivity(
        user: User,
        options: Bundle? = null,
        block: Intent.() -> Unit = {}) {

    launchActivity<LineDetailActivity>(
            options = options,
            init = {
                putExtra(INTENT_USER_ID, user.id)
                block()
            })
}

private const val INTENT_USER_ID = "user_id"

class UserDetailActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val userId = intent.getStringExtra(INTENT_USER_ID)
        requireNotNull(userId) { "no user_id provided in Intent extras" }
    }
}
class OtherActivity: Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //...
        userList.clickListener { user ->

            // super simple, no arguments for simple Activities
            launchUserDetailActivity(user)

            // add custom flags
            launchUserDetailActivity(user) {
                addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
            }

            // use options for cool animations
            val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatar, "avatar")
            launchUserDetailActivity(user = user, options = options)
        }
    }
}

Tuyệt! Mọi thứ dường như đã OK như bên Java rồi, và đừng quên đối tượng argument như trong Java nhé

Thực tế thì ... không phải Tôi đã quên mất một việc khi chỉ đơn giản gọi tới phương thức launchActivity(), nếu không override lại cả chức năng forResult nữa thì method launchUserDetailActivity() với request code sẽ không hoạt động bởi vì Activity sẽ launch trực tiếp và tôi không thể generate ra intent để dử dụng method startActivityForResult(Intent)

Và do đó tôi không muốn viết method này cho tất cả các acitivty nữa. Để làm được thì tôi phải to tay copy từng thứ một, điều này nhàm chán và không thể chấp nhận được.

4. Kết luận từ thực tiễn

Tôi đã từ bỏ việc sử dụng launchActivity và quay trở lại sử dụng method startActivity truyền thống như ở phần 2. Việc này cũng dễ tiếp cận hơn với những người mới làm quen ngôn ngữ Kotlin. Họ có thể tìm kiếm các đoạn code để "startActivity" và cách mà chúng kết nối với nhau. Và việc xây dựng intent builder của tôi vẫn là 1 extension cho Context. Và tôi hài lòng với quyết định này, dù là đôi lúc cảm thấy khá tệ với hàng đống method.

fun Context.UserDetailIntent(user: User): Intent {
    return Intent(this, UserDetailActivity::class.java).apply {
        putExtra(INTENT_USER_ID, user.id)
    }
}

private const val INTENT_USER_ID = "user_id"

class UserDetailActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val userId = intent.getStringExtra(INTENT_USER_ID)
        requireNotNull(userId) { "no user_id provided in Intent extras" }
    }
}
// Usage in other Activities
class OtherActivity: Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //...
        userList.clickListener { user ->
            val options = buildYourOwnOptionsDependingOnYourNeed() // only when required
            startActivity(UserDetailIntent(user), options)
        }
    }
}

5. Bài học

5.1. Thay đổi

  1. Move extra key INTENT_USER_ID ra khỏi Activity. Với các ngôn ngữ khác nhau bạn đều có thể định nghĩa được 1 class chứa các giá trị constant như vậy. Và điều này khác với java, vì nó không cho phép định nghĩa 1 giá trị nào ngoài khối class
  2. Phương thức UserDetailIntent(User) trở thành 1 phương thưc extension
  3. Phương thức apply() dùng để thay thế cho builder trong Kotlin (sử dụng lambda expression)

5.2. Điểm tốt

  1. Inten và các tương tác với nó được dùng ở trong activity luôn
  2. Tạo ra intent ko cần đối tượng Context bên ngoài
  3. chỉ có thể gọi đc trong Context class hoặc trong Context
  4. có thể tùy biến định nghĩa các parameter extra
  5. ko bắt buộc companion object

5.33. Hạn chế

Chưa đưa vào văn bản chính thức newIntent hay newInstance