使用Kotlin&Anko, 扔掉XML开发Android应用

简介:

尝鲜使用Kotlin写了一段时间Android。说大幅度的减少了Java代码一点不夸张。用Java的时候动不动就new一个OnClickListener()匿名类,动不动就类型转换的地方都可以省下很多。更不用说特殊的地方使用data class更是少些不知道多少代码。

Jetbrains给Android带来的不仅是Kotlin,还有Anko。从Anko的官方说明来看这是一个雄心勃勃的要代替XML写Layout的新的开发方式。Anko最重要的一点是引入了DSL(Domain Specific Language)的方式开发Android界面布局。当然,本质是代码实现布局。不过使用Anko完全不用经历Java纯代码写Android的痛苦。因为本身是来自Kotlin的,所以自然的使用这种方式开发就具有了:

  • 类型安全,不再需要那么多的findById()之后的类型转换。
  • null安全,Kotlin里,如果一个变量用?表示为可空,并且使用?之后再调用的时候,即使变量为空也不会引发异常。
  • 无需设备解析XML,因为Anko本质是代码实现的界面和布局,所以省去了这些麻烦。
  • 代码复用,可以通过继承AnkoComponent的方式实现代码复用。XML布局是每一个Activity,每一个View各自专属一个,
    代码复用比较少。

来一个列子看一下。为了不太墨迹,一些不必要的xml声明此处略去。

<RelativeLayout>

    <TextView
        android:id="@+id/sample_text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="Sample text view"
        android:textSize="25sp" />

    <Button
        android:id="@+id/sample_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/sample_text_view"
        android:text="Sample button" />

</RelativeLayout>
    relativeLayout {
        val textView = textView("Sample text view") {
            textSize = 25f
        }.lparams {
            width = matchParent
            alignParentTop()
        }

        button("Sample button").lparams {
            width = matchParent
            below(textView)
        }
    }

准备工作

首先,安装一个Kotlin的插件是必须的。有了这个插件才可以使用Kotlin,然后才可以使用Anko。安装这个插件和Android Studio里安装别的插件市一样的。只需要使用Kotlin查找就可以找到,之后安装即可。

build.gradle里添加下面的代码:

dependencies {
    compile 'org.jetbrains.anko:anko-sdk15:0.8.3' // sdk19, sdk21, sdk23 are also available
    compile 'org.jetbrains.anko:anko-support-v4:0.8.3' // In case you need support-v4 bindings
    compile 'org.jetbrains.anko:anko-appcompat-v7:0.8.3' // For appcompat-v7 bindings
}

然后sync一把。配置的问题解决。

写一个ListView热身

首先创建一个ListView的item点击之后跳转的activity。这里叫做TabDemo1

现在就创建这个listview,并在listview的item点击之后调转到相应的activity去。
这个listview非常简单,只在一个竖直的布局中放置,并且宽度和高度都是填满竖直
布局。

    // 1
    verticalLayout {
        padding = dip(16)
        // 2
        val list = listView() {
            // 3
            adapter = ArrayAdapter<String>(this@MainActivity, android.R.layout.simple_list_item_1, items)
            // 4
            onItemClickListener = object : AdapterView.OnItemClickListener {
                override fun onItemClick(parent: AdapterView<*>?, v: View?, position: Int, id: Long) {
                    when (position) {
                        0 -> {
                            // 5
                            startActivity<TabDemo1>()
                        }
                    }
                }
            }
        }.lparams(width = matchParent) { // 6
            height = matchParent
        }
    }

分别解释:

  1. 竖直布局。本质是LinearLayout,并且orientation的值为vertical。但是
    水平方向的就没有vetialLayout这种可以直接使用的了,需要自己写明orientation。
  2. 创建一个listview。
  3. 给这个listview添加adapter。这里简单实用ArrayAdapter<String>
  4. 添加OnItemClickListenerobject : AdapterView.OnItemClickListener用来
    创建实现某个接口的匿名类。
  5. startActivity<TabDemo1>(),是Anko的语法糖。startActivity(SourceActivity.this, DestActivity.class)
    可以直接简化为startActivity<DestActivity>()。简单了不少。
  6. lparams中设置layout params相关的内容。默认的都是wrap content。这个设置为
    宽、高都为match parent。

用Fragment写一个Tab布局

热身结束。我们来开始真正的开发阶段。

下面要开发的是一个日记App。一共有三个tab,第一个是日记列表,第二个tab是写日记,第三个tab可以设置一些字体大小等(这里只用来占位,不做实现)。

每一个tab都用一个Fragment来展示内容。这三个tab分别HomeListFragmentDetailFragment,DiarySettingsFragment。这个三个fragment都在一个叫做TabDemo1的托管Activity里。

现在就从这个托管activity:TabDemo1开始。这里我们不使用默认的ActionBar,而是用完全自定义的方式来写一个我们自己的action bar。所以需要把界面设定为全屏模式。设置全屏的模式的方法有很多,我们用设置style的方式来实现。

    <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.Light.NoActionBar">
    </style>

之后把这个style应用在activity在AndroidManifest.xml配置中。

这个时候这个托管activity的界面布局就是一个完全的白板了。这个白板现在要分为上中下三部分。上部为我们自定义的action bar,最下面的是tab bar,剩下的部分就是每个tab的内容的fragment。

我们来看一下这个布局应该怎么写:

    // 1
    relativeLayout {
        id = ID_RELATIVELAYOUT

        backgroundColor = Color.LTGRAY

        // 2
        linearLayout {
            id = ID_TOP_BAR
            backgroundColor = ContextCompat.getColor(ctx, R.color.colorPrimary)
            orientation = LinearLayout.HORIZONTAL

            titleTextView = textView {
                text = "Some Title"
                textSize = 16f
                textColor = Color.WHITE
                gravity = Gravity.CENTER_HORIZONTAL or Gravity.CENTER_VERTICAL
            }.lparams {
                width = dip(0)
                height = matchParent
                weight = 1f
            }
        }.lparams {
            width = matchParent
            height = dip(50)
            alignParentTop()
        }

        // 3
        linearLayout {
            id = ID_BOTTOM_TAB_BAR
            orientation = LinearLayout.HORIZONTAL
            backgroundColor = Color.WHITE

            // 4
            homeListTab = weightTextView {
                text = "List"
                normalDrawable = resources.getDrawable(R.mipmap.tab_my_normal)
                selectedDrawable = resources.getDrawable(R.mipmap.tab_my_pressed)
                onClick { tabClick(0) }
            }

            detailTab = weightTextView {
                text = "Detail"
                normalDrawable = resources.getDrawable(R.mipmap.tab_channel_normal)
                selectedDrawable = resources.getDrawable(R.mipmap.tab_channel_pressed)
                onClick { tabClick(1) }
            }

            settingsTab = weightTextView {
                text = "Settings"
                normalDrawable = resources.getDrawable(R.mipmap.tab_better_normal)
                selectedDrawable = resources.getDrawable(R.mipmap.tab_better_pressed)
                onClick { tabClick(2) }
            }

        }.style { // 5
            view ->
            when (view) {
                is TextView -> {
                    view.padding = dip(5)
                    view.compoundDrawablePadding = dip(3)
                    view.textSize = 10f
                    view.gravity = Gravity.CENTER
                }
                else -> {
                }
            }
        }.lparams {
            height = dip(50)
            width = matchParent
            alignParentBottom()
        }

        // 6
        fragmentContainer = frameLayout {
            id = ID_FRAMELAYOUT
            backgroundColor = Color.GREEN
        }.lparams {
            below(ID_TOP_BAR)
            above(ID_BOTTOM_TAB_BAR)
            width = matchParent
            height = matchParent
        }
    }
  1. 前文的例子用了一个verticalLayout, 这里用的是relativeLayout的布局。
  2. 这里是自定义action bar。使用换一个linearLayout。如前所述,要横向布局linear layout
    就需要单独的指定orientation:orientation =LinearLayout.HORIZONTAL。这里比较简单,只有一个显示title的text view。

    这里需要注意gravity = Gravity.CENTER_HORIZONTAL or Gravity.CENTER_VERTICAL
    可以直接写成gravity = Gravity.CENTER。这里是为了突出or的用法。Kotlin里的or
    就是java的|操作符的作用。

  3. 这部分的布局是tab bar。
  4. 这里用的是weightTextView而不是textView。后面会详细的讲解这一部分。
  5. 给tab bar添加style。此style不是彼style。这个style,会遍历tab bar的linear layout内部的全部的view,然后根据when表达式匹配对应的规则,之后给对应于规则的view设置相应的属性。比如,这里会用when语句查看view是否为textView,如果是的话就给这个view设置padding、drawable padding、text size以及gravity属性。tab bar的linear layout有三个text view,所以他们都会被设置这些属性。
  6. 每一个tab的内容展示用fragment就是这里了。准确的说是fragment的container。
    这个container是一个framelayout。在action bar之下,在tab bar之上。在布局的时候有below(ID_TOP_BAR), above(ID_BOTTOM_TAB_BAR)ID_TOP_BARID_BOTTOM_TAB_BAR就分别是action bar和tab bar的id值。这些id值自由设定。

另外,在java写的时候常用的findViewById()方法在Kotlin和Anko中可以改为的find<FrameLayout>(ID_FRAMELAYOUT)。不见得简单,但是增加了类型安全。不用再强制类型转换。也不用担心相关的错误再发生。

上文第4点用到了weightTextView。这是一个自定义的view。在Anko布局中,可以根据自己的需要自定义各种各样的view。但是,需要经过一个小小的处理之后才可以使用到Anko的布局中。这个小小的处理就叫做扩展。下面看看如何给Anko添加weightTextView扩展的。

首先自定义一个view:WeightTextView

class WeightTextView(context: Context) : TextView(context) {
        var normalDrawable: Drawable? = null
        var selectedDrawable: Drawable? = null

        init {
            var layoutParams = LinearLayout.LayoutParams(dip(50),
                    LinearLayout.LayoutParams.MATCH_PARENT, 1f)
            layoutParams.weight = 1f
            this.layoutParams = layoutParams
        }

        override fun setSelected(selected: Boolean) {
            super.setSelected(selected)

            if (selected) {
                this.backgroundColor = ContextCompat.getColor(context, R.color.textGray)
                this.textColor = ContextCompat.getColor(context, R.color.textYellow)

                if (selectedDrawable != null) {
                    this.setCompoundDrawablesWithIntrinsicBounds(null, selectedDrawable, null, null)
                }
            } else {
                this.backgroundColor = ContextCompat.getColor(context, android.R.color.transparent)
                this.textColor = ContextCompat.getColor(context, R.color.textGray)
                if (normalDrawable != null) {
                    this.setCompoundDrawablesWithIntrinsicBounds(null, normalDrawable, null, null)
                }
            }
        }
    }

附加解释:
方法setSelected()是被迫添加的。在使用Anko,相当于使用代码开发Android布局的时候selector不起作用。只好把点击后的高亮效果写在自定义的text view里。

下面看看如何扩展Anko,来使用我们上面的自定义view。

    public inline fun ViewManager.weightTextView() = weightTextView {}
    public inline fun ViewManager.weightTextView(init: WeightTextView.() -> Unit) = ankoView({ WeightTextView(it) }, init)

这部分涉及到的语法内容可以参考官网
这里简单介绍一下。拿官网的例子说一下:

class HTML {
    fun body() { ... }
}

现在有这么一个HTML类,那么调用的时候可以这样:

html {       
    body()  
}

在这么一个lambda表达式里就可以直接这样调用HTML类的方法了,中间的过程是怎么样的呢

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // create the receiver object
    html.init()        
    return html
}

其实灰常的简单呢。在方法html()里,参数是一个HTML类的扩展方法,并且此方法无参,返回Unit(java的void)。

在方法执行的过程中,首先初始化了HTML。之后调用了这个作为参数传入的扩展方法。在具体调用html()方法的时候,可以只简单写一个lambda表达式作为传入的HTML扩展方法。既然是一个类的扩展方法,那当然可以调用这个类内部的方法了。

为了帮助理解,这里给出一个参数是方法的方法:

fun main(args: Array<String>) {
    calling("yo") { p ->
        println("method called $p")
    }

    calling("yoyo", ::called)
}


fun calling(param: String, func: (String) -> Unit) {
    func(param)
}

fun called(p: String) {
    println("output string $p")
}

第一个是用lambda表达式作为传入方法,第二个是已经定义好的一个方法作为传入方法。

Fragment的处理

本文中的重点在于使用Anko做布局,具体的逻辑处理java写和Kotlin写没有什么区别。这里只简单介绍一下。

为了保证兼容,这里使用Support v4来处理Fragment的显示等操作。在activity的一开始就把需要的fragemnt都加载进来。

    fun prepareTabFragments() {
        val fm = supportFragmentManager
        homeListFragment = HomeListFragment.newInstance()
        fm.beginTransaction()
                .add(ID_FRAMELAYOUT, homeListFragment)
                .commit()
        detailFragment = DetailFragment.newInstance(null)
        detailFragment?.modelChangeListener = homeListFragment
        fm.beginTransaction()
                .add(ID_FRAMELAYOUT, detailFragment)
                .commit()
        settingsFragment = DiarySettingsFragment.newInstance()
        fm.beginTransaction()
                .add(ID_FRAMELAYOUT, settingsFragment)
                .commit()
    }

每一个tab项被点击的时候的处理:

    fun tabClick(index: Int) {
        info("index is $index")
        val ft = supportFragmentManager.beginTransaction()
        ft.hide(homeListFragment)
        ft.hide(detailFragment)
        ft.hide(settingsFragment)

        // unselect all textviews
        homeListTab?.isSelected = false
        detailTab?.isSelected = false
        settingsTab?.isSelected = false

        when (index) {
            0 -> {
                homeListTab?.isSelected = true
                ft.show(homeListFragment)
            }
            1 -> {
                detailTab?.isSelected = true
                ft.show(detailFragment)
            }
            2 -> {
                settingsTab?.isSelected = true
                ft.show(settingsFragment)
            }
            else -> {

            }
        }

        ft.commit()
    }

分别开始每一个Fragment

在开始之前需要考虑一个很严重的事情:数据存在什么地方。本来应该是SQLite或者存在云上的。存在云裳就可以实现同一个账号登录在任何地方都可以同步到同样的内容。这里只简单模拟,存放在app的内存里。存放在Application派生类AnkoApplication
静态属性diaryDataSource里。diaryDataSource是一个ArrayList一样的列表。

class AnkoApplication : Application() {

    override fun onCreate() {
        super.onCreate()
    }

    companion object {
        var diaryDataSource = mutableListOf<DiaryModel>()
    }
}

第一个tab,HomeListFragment

HomeListFragment类作为第一个tab内容展示fragment,用来显示全部的日记列表的布局就非常简单了,和我们前面的例子没有什么太大的差别。就是在一个verticalLayout里放一个list view。这个list view的data source只需要一个列表。

    // 1
    var view = with(ctx) {
        verticalLayout {
            backgroundColor = Color.WHITE

            listView = listView {
                adapter = ArrayAdapter<DiaryModel>(ctx,
                        android.R.layout.simple_list_item_1,
                        AnkoApplication.diaryDataSource)

                onItemClick { adapterView, view, i, l ->
                    toast("clicked index: $i, content: ${AnkoApplication.diaryDataSource[i].toString()}")
                }
            }

            // 2
            emptyTextView = textView {
                text = resources.getString(R.string.list_view_empty)
                textSize = 30f
                gravity = Gravity.CENTER
            }.lparams {
                width = matchParent
                height = matchParent
            }
        }
    }
    // 3
    listView?.emptyView = emptyTextView
    
    return view
  1. 在activity里的布局可以直接写vertical{},但是在fragment里不可以这样。直接写vertical{}就已经把这个layout添加到父view上了,这fragment里是不行的。在fragment里需要创建一个单独的view,并返回。用with语句来创建这样一个单独的view。
  2. 在vertial layout里添加了一个textview。
  3. 上面一步创建的textview作为list view没有数据的时候显示的empty view来使用。

第二个tab,DetailFragment

日记的内容包括,日记title,日记本身的内容还有日记的日期。

所以布局上就包括日记的title、内容输入用的EditText以及为了说明用的text view,还有edit text里的hint。最后还有一个选择
日期的控件。

    return with(ctx) {
        verticalLayout {
            padding = dip(10)
            backgroundColor = Color.WHITE
            textView("TITLE") {

            }.lparams(width = matchParent)

            titleEditText = editText {
                hint = currentDateString()
                lines = 1
            }.lparams(width = matchParent) {
                topMargin = dip(5)
            }

            textView("CONTENT") {

            }.lparams(width = matchParent) {
                topMargin = dip(15)
            }

            contentEditText = editText {
                hint = "what's going on..."
                setHorizontallyScrolling(false)
            }.lparams(width = matchParent) {
                //                    height = matchParent
                topMargin = dip(5)
            }

            button(R.string.button_select_time) {
                gravity = Gravity.CENTER
                onClick {
                    val fm = activity.supportFragmentManager
                    var datePicker = DatePickerFragment.newInstance(diaryModel?.date)
                    datePicker.setTargetFragment(this@DetailFragment, DetailFragment.REQUEST_DATE)
                    datePicker.show(fm, "date")
                }
            }
            // *
            button(R.string.button_detail_ok) {
                onClick {
                    v ->
                    println("ok button clicked")
                    try {
                        var model = diaryModel!!
                        model.title = titleEditText?.text.toString()
                        model.content = contentEditText?.text.toString()
                        AnkoApplication.diaryDataSource.add(model)

                        modelChangeListener?.modelChanged()

                        toast(R.string.model_saved_ok)
                    } catch(e: Exception) {
                        Log.d("##DetailFragment", "error: ${e.toString()}")
                        toast(R.string.model_save_error)
                    }
                }
            }.lparams {
                topMargin = dip(10)
                width = matchParent
            }
        }.style {
            view ->
            when (view) {
                is Button -> {
                    view.gravity = Gravity.CENTER
                }
                is TextView -> {
                    view.gravity = Gravity.LEFT
                    view.textSize = 20f
                    view.textColor = Color.DKGRAY
                }
            }
        }
    }

需要注意打星号的地方。按钮在点击之后会弹出一个dialog fragment来显示日期view。用户可以在这个日期view里选择相应的日期。但是,如何从日期dialog fragment传递选择的日期给DetailFragment呢?这里就涉及到两个fragment之间传递数据的问题。

选择日期的dialog fragment是DatePickerFragment

    var pickerView = DatePicker(activity)
    pickerView.calendarViewShown = false
    pickerView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT)
    pickerView.init(year, month, day) {
        view, year, month, day ->
        mDate = GregorianCalendar(year, month, day).time

        arguments.putSerializable(EXTRA_DATE, mDate)
    }

    return AlertDialog.Builder(activity)
            .setView(pickerView)
            .setTitle(R.string.date_picker_title)
            .setPositiveButton(R.string.picker_button_ok) { dialog, which ->
                toast("hello world!")
                sendResult(Activity.RESULT_OK)
            }.create()

首先DatePickerFragment要继承DialogFragment之后override方法onCreateDialog(savedInstanceState: Bundle)。在这个方法里使用上面代码创建一个包含日期选择器的dialog。

在选择日期的时候,会触发DatePickerOnDateChangedListener接口的onDateChanged方法。我们在这个方法里记录选择好的日期数据,在dialog的positive按钮点击之后把这个数据发送给DetailFragment

那么怎么发送呢?使用target fargment方法。在detail fragment弹出dialog fragment的时候,把detail fragment设置为target fragment。

button(R.string.button_select_time) {
    gravity = Gravity.CENTER
    onClick {
        val fm = activity.supportFragmentManager
        var datePicker = DatePickerFragment.newInstance(diaryModel?.date)
        // *
        datePicker.setTargetFragment(this@DetailFragment, DetailFragment.REQUEST_DATE)
        datePicker.show(fm, "date")
    }
}

在标星下面的一行代码中。datePicker.setTargetFragment(this@DetailFragment,DetailFragment.REQUEST_DATE)DetailFragment设定为target fragment,并且指定REQUEST_DATE这code,为以后取出数据使用。

    companion object Factory {
        val REQUEST_DATE = 0`
    }

在positive按钮点击之后执行方法sendResult回传数据

    private fun sendResult(resultCode: Int) {
        if (targetFragment == null)
            return

        var i = Intent()
        i.putExtra(EXTRA_DATE, mDate)
        // *
        targetFragment.onActivityResult(targetRequestCode, resultCode, i)
    }

调用targetFragmentonActivityResult()方法来回传日期数据。

DetailFragment中通过override方法onActivityResult()来接收数据。

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (resultCode != Activity.RESULT_OK) {
            return
        }

        if (requestCode != REQUEST_DATE) {
            return
        }

        var date = data?.getSerializableExtra(DatePickerFragment.EXTRA_DATE) as Date
        diaryModel?.date = date
    }

日期数据传输这部分到这里结束。

全文也可以在这里画上一个句点了。以上还有很多关于Anko没有使用的地方。Anko也是可以实现代码界面分离的。继承AnkoComponent可以写出独立的布局文件,并且可以用anko preview插件来预览界面效果。就拿setting这个tab的fragment来举例:
首先定义一个独立的布局文件:

class SettingsUI<T> : AnkoComponent<T> {
    override fun createView(ui: AnkoContext<T>) = with(ui) {
        verticalLayout {
            backgroundColor = ContextCompat.getColor(ctx, R.color.SnowWhite)
            textView { text = resources.getString(R.string.settings_title) }

            button("activity with the same `AnkoComponent`") {
                id = ID_BUTTON
            }
        }
    }

    companion object Factory {
        public val ID_BUTTON = 101
    }
}

把这个布局文件用在DiarySettingsFragment上:

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
        val view = SettingsUI<DiarySettingsFragment>().createView(AnkoContext.create(ctx, DiarySettingsFragment()))

        return view
    }

然后这个布局还可以用在我们刚刚创建的TempActivity上:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        SettingsUI<TempActivity>().setContentView(this)

        val button = find<Button>(SettingsUI.ID_BUTTON)
        button.text = "you are in `TempActivity`, CLICK!"

        button.onClick {
            toast("${TempActivity::class.java.simpleName}")
        }
    }

Activity上使用就简单很多了,只需要这么一句SettingsUI<TempActivity>().setContentView(this)

代码在这里。除了布局Anko还有其他的一些语法糖糖也很是不错,不过这里就不多说了。有更多想了解的,请移步官网

欢迎加群互相学习,共同进步。QQ群:iOS: 58099570 | Android: 572064792 | Nodejs:329118122 做人要厚道,转载请注明出处!












本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/sunshine-anycall/p/5300305.html ,如需转载请自行联系原作者

相关文章
|
13天前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
13天前
|
存储 搜索推荐 Java
打造个性化安卓应用:从设计到实现
【10月更文挑战第30天】在数字化时代,拥有一个个性化的安卓应用不仅能够提升用户体验,还能加强品牌识别度。本文将引导您了解如何从零开始设计和实现一个安卓应用,涵盖用户界面设计、功能开发和性能优化等关键环节。我们将以一个简单的记事本应用为例,展示如何通过Android Studio工具和Java语言实现基本功能,同时确保应用流畅运行。无论您是初学者还是希望提升现有技能的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧。
|
17天前
|
搜索推荐 开发工具 Android开发
打造个性化Android应用:从设计到实现的旅程
【10月更文挑战第26天】在这个数字时代,拥有一个能够脱颖而出的移动应用是成功的关键。本文将引导您了解如何从概念化阶段出发,通过设计、开发直至发布,一步步构建一个既美观又实用的Android应用。我们将探讨用户体验(UX)设计的重要性,介绍Android开发的核心组件,并通过实际案例展示如何克服开发中的挑战。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧,帮助您在竞争激烈的应用市场中脱颖而出。
|
19天前
|
算法 Java 数据库
Android 应用的主线程在什么情况下会被阻塞?
【10月更文挑战第20天】为了避免主线程阻塞,我们需要合理地设计和优化应用的代码。将耗时操作移到后台线程执行,使用异步任务、线程池等技术来提高应用的并发处理能力。同时,要注意避免出现死循环、不合理的锁使用等问题。通过这些措施,可以确保主线程能够高效地运行,提供流畅的用户体验。
31 2
|
23天前
|
Java API Android开发
安卓应用程序开发的新手指南:从零开始构建你的第一个应用
【10月更文挑战第20天】在这个数字技术不断进步的时代,掌握移动应用开发技能无疑打开了一扇通往创新世界的大门。对于初学者来说,了解并学习如何从无到有构建一个安卓应用是至关重要的第一步。本文将为你提供一份详尽的入门指南,帮助你理解安卓开发的基础知识,并通过实际示例引导你完成第一个简单的应用项目。无论你是编程新手还是希望扩展你的技能集,这份指南都将是你宝贵的资源。
48 5
|
23天前
|
移动开发 Dart 搜索推荐
打造个性化安卓应用:从零开始的Flutter之旅
【10月更文挑战第20天】本文将引导你开启Flutter开发之旅,通过简单易懂的语言和步骤,让你了解如何从零开始构建一个安卓应用。我们将一起探索Flutter的魅力,实现快速开发,并见证代码示例如何生动地转化为用户界面。无论你是编程新手还是希望扩展技能的开发者,这篇文章都将为你提供价值。
|
24天前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
28 1
|
26天前
|
JavaScript Java Kotlin
Kotlin开发笔记 - 常量与变量
Kotlin开发笔记 - 常量与变量
29 2
|
26天前
|
XML 存储 数据库
XML在数据库中有哪些应用?
【10月更文挑战第17天】XML在数据库中有哪些应用?
26 2
|
27天前
|
JavaScript Java Kotlin