“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。
Google最近为Kotlin授予了Android官方支持状态,但对于许多开发人员而言,它仍然很难理解。 最好的开始方法是自己创建一个完整的应用程序,这将在本教程中进行。 在本教程中,您将使用Spring Boot作为支持Android(+ Kotlin)移动应用程序的API。 Spring Boot是用最少的代码创建健壮的REST API的好方法。
我将假设您具有Java经验,并且至少在创建Android应用方面有过经验。 如果您没有任何Android经验,则应该可以继续学习,但您可能需要在Google那里到那里做一些事情。
如果您想直接到最后,这里是完整的代码 。
在开始之前,让我们先谈谈Kotlin。
Kotlin与Java
Kotlin对新来者看起来很奇怪。 它类似于您可能已经见过的其他语言,但是有些事情看起来不对劲,通常是因为它是如此的简洁!
不要惊慌-因为它是如此的可扩展,所以有很多方法可以编写相同的代码,并且许多其他语言都没有捷径。 例如,通常您会看到大括号用作函数参数:
dialogBuilder.setPositiveButton("Delete", { dialog, whichButton ->
deleteMovie(movie)
})
这实际上是在创建一个匿名函数( lambda )并将其传递。此函数接受两个在此推断出的参数。 看一看等效的(JRE 8之前的)Java代码:
dialogBuilder.setPositiveButton("Delete",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
deleteMovie(movie);
}
}
);
(当然,现在Java 8也具有lambdas )。
这是我们稍后将使用的一些代码的另一个示例:
class MovieViewHolder(val view: View) : RecyclerView.ViewHolder(view)
为了理解这一点,您必须了解几件事:
用括号声明一个类(即(view: View)
)意味着您正在声明该类的主要构造函数 (是的,也有次要构造函数 )。 冒号:
与implements
或extends
相似,但实际上是关于接口的 。 在主构造函数中声明的所有内容都会自动声明为属性(成员变量)。
为了清楚起见,这是等效的Java:
public static class MovieViewHolder extends RecyclerView.ViewHolder {
public final View view;
public MovieViewHolder(View v) {
super(v);
view = v;
}
}
作为最后一个示例,请看以下bean:
package demo
data class Movie( val id: Int, val name: String )
那是完整的文件。 它声明一个带有构造函数的类,两个只读属性(成员变量),并在构造函数中分配这些属性。 然后, data
会为我们的所有成员变量以及equals()
, toString()
和其他变量创建getter和setter方法( 请参阅此处 ,是否要在完整的Java荣耀中看到它)。
现在您已经有了一些背景,让我们开始吧!
为您的Android + Kotlin项目创建Spring Boot API
官方的Spring Boot教程建议您使用Initializr网站创建启动框架,但是我发现从头开始构建项目会更容易。
首先,使用Gradle初始化一个空目录(确保已安装Gradle并在命令行上可用)。
C:\Users\Karl\Kotlin-Spring>gradle init
BUILD SUCCESSFUL in 3s
2 actionable tasks: 2 executed
C:\Users\Karl\Kotlin-Spring>
您应该有两个文件夹和四个文件。
.
+-- build.gradle
+-- gradle
¦ L-- wrapper
¦ +-- gradle-wrapper.jar
¦ L-- gradle-wrapper.properties
+-- gradlew
+-- gradlew.bat
L-- settings.gradle
2 directories, 6 files
现在将build.gradle
更改为以下内容:
buildscript {
ext.kotlin_version = '1.2.61' // Required for Kotlin integration
ext.spring_boot_version = '2.0.2.RELEASE'
repositories {
jcenter()
}
dependencies {
// Required for Kotlin integration
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
classpath("org.jetbrains.kotlin:kotlin-noarg:$kotlin_version")
classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version"
}
}
// Required for Kotlin integration
apply plugin: 'kotlin'
// See https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin
apply plugin: "kotlin-spring"
apply plugin: 'kotlin-jpa'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
jar {
baseName = 'kotlin-demo'
version = '0.1.0-SNAPSHOT'
}
repositories {
jcenter()
}
dependencies {
// Required for Kotlin integration
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect" // For reflection
compile 'org.springframework.boot:spring-boot-starter-data-rest'
compile 'org.springframework.boot:spring-boot-starter-data-jpa'
compile 'com.h2database:h2'
}
这里导入了Kotlin和Spring Boot插件,声明了外部存储库,并添加了依赖库。
如果您还没有使用过Spring Boot,那么您应该知道它(或者说是Spring Framework)在运行时使用了依赖注入。 这意味着将根据您导入的库自动连接整个应用程序。 例如,在build.gradle
的结尾,您将看到Data REST和Data JPA库。 当Spring Boot看到两个库时,它将自动将您的应用程序配置为REST服务器。 此外,由于包含了H2
数据库库,因此Spring将使用H2数据库引擎来持久化我们传入或传出查询的任何REST数据。
拥有一个完整的REST应用程序所需要的就是定义一个带有@SpringBootApplication
批注的类。 您甚至不需要指定它的路径-Spring会搜索它!
将以下内容放入src/main/kotlin/demo/Application.kt
:
package demo
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
class Application
fun main(args: Array) {
SpringApplication.run(Application::class.java, *args)
}
现在,如果您运行gradlew bootRun
(在* nix上运行./gradlew bootRun
),则所有内容都应构建(并下载),并且您应该在巨大的日志Started Application中看到某处。 现在,在另一个窗口中运行curl
以查看发生了什么。
C:\Users\Karl>curl localhost:8080
{
"_links" : {
"profile" : {
"href" : "http://localhost:8080/profile"
}
}
}
令人惊讶的是,您已经通过Kotlin创建了一个完全兼容的REST服务器,只需编辑两个文件即可!
使用Kotlin添加对象
要创建对象,您只需要实体类和存储库。
在Application.kt
旁边,将以下内容放入Model.kt
package demo
import javax.persistence.*
@Entity
data class Movie(@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val Id: Long,
val name: String)
在这里,您已使用数据惯用法为所有属性创建获取器和设置器,以及使用JPA批注指定如何为您的实体生成ID。
注意: Id
字段必须以大写字母I开头。 如果不是,则服务器在执行查询时不会返回id字段。 在连接到客户端应用程序时,这会给您带来麻烦。
现在将其放入Repository.kt
:
package demo
import org.springframework.data.repository.CrudRepository
interface ItemRepository : CrudRepository<Movie, Long>
您已经完成了! 令人难以置信的是,我们现在可以在此服务器上执行任何CRUD操作,并且它将对数据库进行所有更改,并且可以正常运行。
C:\Users\Karl>curl -X POST -H "Content-Type:application/json" -d " {\"name\":\"The 40 Year Old Virgin\"} " localhost:8080/movies
{
"name" : "The 40 Year Old Virgin",
"_links" : {
"self" : {
"href" : "http://localhost:8080/movies/1"
},
"item" : {
"href" : "http://localhost:8080/movies/1"
}
}
}
C:\Users\Karl>curl localhost:8080/movies/1
{
"name" : "The 40 Year Old Virgin",
"_links" : {
"self" : {
"href" : "http://localhost:8080/movies/1"
},
"item" : {
"href" : "http://localhost:8080/movies/1"
}
}
}
在您的Kotlin应用程序中加载初始数据
最后,让我们加载一些数据。 同样,与Spring Boot一样,一切都可以轻松完成。 只需将以下内容放入src/main/resources/data.sql
,它将在启动时运行。
INSERT INTO movie (name) VALUES
('Skyfall'),
('Casino Royale'),
('Spectre');
要确认它是否正常运行,请重新启动服务器并运行curl localhost:8080/movies
。
后端就完成了。 是时候建立客户了。
使用Kotlin构建您的Android应用
这将需要几个步骤:首先,您将使用Android Studio创建一个空的Kotlin应用程序。 然后,您将使用RecyclerView创建一个列表视图(带有添加,编辑和删除按钮),并在其中填充硬编码数据。 最后,您将使用Retrofit将视图连接到刚创建的REST后端。
在Android Studio中创建一个项目。 确保您使用的版本至少为Android Studio3 。除了确保包含Kotlin支持外,每个窗口均使用默认值。 为项目命名,随心所欲–我将其命名为“ Kotlin Crud”。 最后,选择一个清空活动 。
当您在顶部图标栏上按“播放”时,在运行Hello World时,您应该会看到它(您可以插入手机或在模拟器上运行。在线检查如何进行设置)。
如果您在使用Java之前制作了Android应用程序,您会注意到唯一的区别是主要活动:它称为MainActivity.kt ,而不是MainActivity.java
,并且代码看起来有些不同。
package demo
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
区别如下:
- 该类未指定为
public
(在Kotlin中,这是默认设置) - 用冒号指定类型
:
–类的类型为AppCompatActivity
(或者,如您在Java中所说,它实现了AppCompatActivity
),而savedInstanceState
的类型为Bundle
- 方法只是被称为
fun
而不是function -
override
不是注释 - 问号表示参数是可选的 (在Java中是不可能的)
最后一点是讨论Kotlin与Java重要性时讨论最多的问题之一:这是该语言确保null安全的多种方式之一。
导入其他Android库
您需要在应用程序的build.gradle
文件中添加额外的库:一个用于回收站视图(将在第二个视图中使用),一个用于卡片视图,另一个用于浮动操作按钮。 将它们放在“ dependencies
部分中的其他项旁边。
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:recyclerview-v7:27.1.1'
Android Studio应该要求您立即同步 。 单击该按钮,可以看到所有内容都构建正确。
注意:确保版本与其他支持库相同(例如appcompat-v7:27.1.1)。 另外,由于将使用内置图标(以后应避免使用),因此您还需要将以下内容放入build.gradle
的defaultConfig
部分中。
vectorDrawables.useSupportLibrary = true
在Kotlin中添加图标
您需要一些按钮图标-一个用于添加 ,另一个用于刷新 。 转到“ 材料图标”站点,然后选择您喜欢的一个。 我正在选择添加按钮的一半。 当您单击它时,灰色和蓝色的下载部分应出现在左侧的按钮上。 单击灰色框“ 选定的图标”控件以打开下载选项。 现在应该有一个下拉列表,您可以在其中选择Android作为类型。
将颜色更改为白色,然后下载PNG选项。 将ZIP文件的内容提取到app/src/main
(您应该看到ZIP文件中包含一个res
文件夹)。
现在,您可以在布局中使用新图标。 它们被称为baseline_add_white_36
_add_white_36之类的东西。
最后,对循环图标(也是白色)执行相同的操作。
创建视图XML
每个列表项都需要一个XML视图。 将以下内容放入src/main/res/layout/list_item.xml
。
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="5dp"
android:padding="3dp"
card_view:cardElevation="2dp"
card_view:cardMaxElevation="2dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="lastname"
android:textSize="16dp" />
<TextView
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:drawableLeft="@android:drawable/ic_delete"
android:padding="5dp" />
<TextView
android:id="@+id/btnEdit"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginRight="2dp"
android:layout_toLeftOf="@+id/btnDelete"
android:drawableLeft="@android:drawable/ic_menu_edit"
android:padding="5dp" />
</RelativeLayout>
</android.support.v7.widget.CardView>
在这里,您使用的是Card View ,这是在Android中创建列表的常用方法。 几乎所有的XML都是布局设置,以确保正确对齐。 请注意您用于将其连接到我们的Kotlin文件的android:id
值。 另外,我在编辑和删除按钮中使用了一些内置的Android图标。
注意:不建议这样做,因为这些图标可以在Android Studio版本之间进行更改–就像我们之前那样下载图标!
现在开始主要活动XML。 这是src/main/res/layout/activity_main.xml
外观。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_item_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/baseline_add_white_36"
android:layout_gravity="bottom|end"
app:elevation="6dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_margin="20dp"/>
</RelativeLayout>
非常简单。 现在,您已经在相对布局内获得了一个回收视图和一个浮动操作按钮,并已将baseline_add_white_36
分配为该按钮的源。 请注意,回收站视图的ID为rv_list_item
(您将很快使用它)。
将刷新添加到操作栏
要填写内容,请在操作栏上放置一个刷新按钮。 这需要在res/menu/buttons.xml
新的XML:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/refresh"
android:icon="@drawable/baseline_loop_white_48"
android:title="@string/refresh"
app:showAsAction="ifRoom"/>
</menu>
请注意,它具有一个名为refresh的ID。 另外,我使用了Android Icons网站(白色变体)中的循环图标-您必须像以前一样下载它。 另外,我正在使用资源中的字符串,因此您必须更改res/values/strings.xml
:
<resources>
<string name="app_name">Kotlin Crud</string>
<string name="refresh">Refresh</string>
</resources>
在Kotlin的显示列表
现在使用我们的视图显示项目列表。 做到这一点的规范方法是相对较新的RecyclerView
,它取代了原来的ListView
。 RecyclerView
的基本思想是仅创建足以在屏幕上显示的视图–如果屏幕可以容纳五个项目,则仅创建五个。 在列表中滚动时,这些视图将被重新使用(回收),并用适当的(新)值替换其内容。
您如何开始呢? 您需要的第一件事是一个bean。 我们称它为Movie.kt
。
package demo
data class Movie( val id: Int, val name: String )
注意:对于以下所有类,请确保package
与MainActivity.kt
的package
匹配。
那不容易吗? 接下来,您需要一个Adapter
。 这是一个具有三种方法的类:一种用于返回总共显示多少个项目( getItemCount()
),一种用于为特定项目创建Android View控件( onCreateViewHolder()
),以及一种使用以下方法填充现有视图的方法:您的数据实例( onBindViewHolder()
)。
将其放入MovieAdapter.kt
。
class MovieAdapter : RecyclerView.Adapter() {
var movies: ArrayList = ArrayList()
init { refreshMovies() }
class MovieViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup,
viewType: Int): MovieAdapter.MovieViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return MovieViewHolder(view)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.view.name.text = movies[position].name
}
override fun getItemCount() = movies.size
fun refreshMovies() {
movies.clear()
movies.add(Movie(0, "Guardians of the Galaxy"))
movies.add(Movie(1, "Avengers: Infinity War"))
movies.add(Movie(2,"Thor: Ragnorok"))
notifyDataSetChanged()
}
}
将其粘贴到Android Studio中时,它将以红色突出显示某些内容。 您需要多次按ALT-ENTER(在Mac上为Option + Enter)才能导入所需的导入。 最终,这是您应具有的进口清单:
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.list_item.view.*
MovieAdapter.kt
发生了很多事情。 除了刚刚实施来实现所需的三种方法RecylcerView.Adapter
,你创建了一个名为的属性movies
中,一个列表,并初始化它init{}
构造函数。 此外,您还声明了一个内部类,名为MovieViewHolder
。 这就是为每个需要显示的视图实例化的内容(在所讨论的示例中为五个视图)。 如您所见, onCreateViewHolder
实际上返回此类型的对象。 该类非常简单–将一个View
(现在也是一个属性)放入其构造函数中,并返回Holder
类型的对象。 在使用onBindViewHolder
填写数据时,将使用此对象-在我们的示例中,是设置显示的文本。
起初这似乎很复杂。 观察以下所有方面的好方法:它如何连接到您的主代码类(即MainActivity.kt
),以及如何连接到您在XML中定义的视图?
对于第一部分,主要活动现在应该是这样的:
class MainActivity : AppCompatActivity() {
lateinit var adapter:MovieAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
adapter = MovieAdapter()
rv_item_list.layoutManager = LinearLayoutManager(this)
rv_item_list.adapter = adapter
}
}
因此,在这里,您已将adapter
定义为lateinit变量– lateinit
告诉Kotlin您要在创建后的某个阶段初始化它,而不是Kotlin类中的默认值–通常,您必须立即初始化。
在构造函数中,您将适配器的实例分配给该属性(请注意,您无需在Kotlin中使用new
),并为rv_item_list
分配两件事– LayoutManager (用于定位)和Adapter(我们将ve刚刚创建)。
我们应该谈论rv_item_list
。 这只是activity_main.xml
内部控件的ID,特别是recyclerview。 通常,您需要使用findViewById
(这对Android开发人员来说是一个痛苦),但是对于Kotlin,您只需指定其名称即可。 当Android Studio抱怨导入并且您ALT-ENTER(或与您的平台等效)时,它将自动导入kotlinx.android.synthetic.main.activity_main.*
,将所有ID引入命名空间。
最后,将以下两个功能添加到MainActivity
:
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.buttons, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.refresh -> {
adapter.refreshMovies()
Toast.makeText(this.baseContext, "Refreshed", Toast.LENGTH_LONG).show())
true
}
else -> {
super.onOptionsItemSelected(item)
}
}
这将使您定义的菜单xml膨胀,并将按钮与适配器的刷新功能联系在一起(并方便举杯说它可以工作)。
就是这样! 运行我们的代码,您应该看到以下内容。
连线您的Android + Kotlin应用程式
接下来,您需要用来自API服务器的数据替换硬编码的值,并将不同的按钮连接到各自的API调用。 为此,您将使用Square的Retrofit库。
首先将以下内容添加到build.gradle
依赖项中:
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
现在看一下在呼叫服务器以获取电影列表时发生的情况:
C:\Users\Karl>curl http://localhost:8080/movies
{
"_embedded" : {
"movies" : [ {
"name" : "Skyfall",
"id" : 1,
"_links" : {
"self" : {
"href" : "http://localhost:8080/movies/1"
},
"movie" : {
"href" : "http://localhost:8080/movies/1"
}
}
}
我只显示了一个,因为它很长(Spring遵循一个称为HATEOAS的东西,它添加了指向json响应的链接)。 正如你所看到的,应对被包裹在一个_embedded
对象,你的电影来为列表中的movies
。 您需要在Kotlin模型中表示这一点,以便Retrofit知道会发生什么。 将Movie.kt
更改为此:
import com.google.gson.annotations.SerializedName
data class Movie( val id: Int, val name: String )
data class MovieList (
@SerializedName("movies" )
val movies: List
)
data class MovieEmbedded (
@SerializedName("_embedded" )
val list: MovieList
)
现在,您需要创建一个新类来设置Retrofit。 我们称它为MovieApiClient.kt
:
import io.reactivex.Completable
import io.reactivex.Observable
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*
interface MovieApiClient {
@GET("movies") fun getMovies(): Observable
@POST("movies") fun addMovie(@Body movie: Movie): Completable
@DELETE("movies/{id}") fun deleteMovie(@Path("id") id: Int) : Completable
@PUT("movies/{id}") fun updateMovie(@Path("id")id: Int, @Body movie: Movie) : Completable
companion object {
fun create(): MovieApiClient {
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://10.0.2.2:8080/")
.build()
return retrofit.create(MovieApiClient::class.java)
}
}
}
在这里,您可以使用批注及其预期的返回类型来定义所有端点( Completable
,是RxJava的一部分,仅表示未返回任何内容)。 您还声明了一个伴随对象 (类似于静态类),该对象使用API的详细信息实例化Retrofit构建器。 请注意,基本URL使用IP 10.0.2.2
,该IP 10.0.2.2
允许仿真器连接到localhost。
现在,在MovieAdapter
更改标头以包括context
属性(以便您可以为我们的API结果附加敬酒),以及添加使用先前的create()
方法初始化的惰性客户端属性。
class MovieAdapter(val context: Context) : RecyclerView.Adapter() {
val client by lazy { MovieApiClient.create() }
var movies: ArrayList = ArrayList()
懒惰接受一个函数(请注意大括号)并说“当某人首次尝试使用此属性时,请运行该函数并进行分配”。
要初始化上下文,请更改adapter
initialize语句以包含主要活动上下文:
adapter = MovieAdapter(this.baseContext)
现在,将适配器中的refreshMovies()
更改为以下内容:
fun refreshMovies() {
client.getMovies()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
movies.clear()
movies.addAll(result.list.movies)
notifyDataSetChanged()
},{ error ->
Toast.makeText(context, "Refresh error: ${error.message}", Toast.LENGTH_LONG).show()
Log.e("ERRORS", error.message)
})
}
因此,您正在使用客户端的getMovies()
函数,该函数在MovieApiClient.kt
的顶部MovieApiClient.kt
。 要了解这里发生的事情,需要自己进行整个讨论。 基本上,它使用的是反应式编程 ,这是一种将异步内容(例如,调用外部API)连接在一起的新方法。
对于其余的访问方法,将以下内容放在refreshMovies()
:
fun updateMovie(movie: Movie) {
client.updateMovie(movie.id, movie)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ refreshMovies() }, { throwable ->
Toast.makeText(context, "Update error: ${throwable.message}", Toast.LENGTH_LONG).show()
})
}
fun addMovie(movie: Movie) {
client.addMovie(movie)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ refreshMovies() }, { throwable ->
Toast.makeText(context, "Add error: ${throwable.message}", Toast.LENGTH_LONG).show()
})
}
fun deleteMovie(movie: Movie) {
client.deleteMovie(movie.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ refreshMovies() }, { throwable ->
Toast.makeText(context, "Delete error: ${throwable.message}", Toast.LENGTH_LONG).show()
})
}
在这里,您可以像以前一样使用Retrofit反应式调用,但是,当事情成功返回时(如果不这样做,请refreshMovies()
,而只需调用refreshMovies()
)即可。
在Kotlin中显示对话框
您需要做的最后一件事是显示各种输入情况的对话框:删除,编辑和添加。 在这里,您将手动执行此操作,因此无需创建任何新的XML。
在MainActivity.kt
添加以下功能:
fun showNewDialog() {
val dialogBuilder = AlertDialog.Builder(this)
val input = EditText(this@MainActivity)
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT)
input.layoutParams = lp
dialogBuilder.setView(input)
dialogBuilder.setTitle("New Movie")
dialogBuilder.setMessage("Enter Name Below")
dialogBuilder.setPositiveButton("Save", { dialog, whichButton ->
adapter.addMovie(Movie(0,input.text.toString()))
})
dialogBuilder.setNegativeButton("Cancel", { dialog, whichButton ->
//pass
})
val b = dialogBuilder.create()
b.show()
}
此处, 对话框生成器用于显示标准弹出窗口。 您还手动添加了EditText
控件,以便用户可以输入新名称。 侦听器使用“正”和“负”按钮(请参阅上一链接的“添加按钮”部分),并在发生“正”按钮(对话框确认)时调用适配器的addMovie
函数。
要确保单击操作按钮时弹出此对话框,请将以下内容置于MainActivity.onCreate()
函数的底部:
fab.setOnClickListener{ showNewDialog() }
这是其他对话框的代码,我们将它们放入MovieAdapter
:
fun showUpdateDialog(holder: MovieViewHolder, movie: Movie) {
val dialogBuilder = AlertDialog.Builder(holder.view.context)
val input = EditText(holder.view.context)
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT)
input.layoutParams = lp
input.setText(movie.name)
dialogBuilder.setView(input)
dialogBuilder.setTitle("Update Movie")
dialogBuilder.setPositiveButton("Update", { dialog, whichButton ->
updateMovie(Movie(movie.id,input.text.toString()))
})
dialogBuilder.setNegativeButton("Cancel", { dialog, whichButton ->
dialog.cancel()
})
val b = dialogBuilder.create()
b.show()
}
fun showDeleteDialog(holder: MovieViewHolder, movie: Movie) {
val dialogBuilder = AlertDialog.Builder(holder.view.context)
dialogBuilder.setTitle("Delete")
dialogBuilder.setMessage("Confirm delete?")
dialogBuilder.setPositiveButton("Delete", { dialog, whichButton ->
deleteMovie(movie)
})
dialogBuilder.setNegativeButton("Cancel", { dialog, whichButton ->
dialog.cancel()
})
val b = dialogBuilder.create()
b.show()
}
要将它们连接起来,请将以下内容添加到onBindViewHolder
方法中:
holder.view.btnDelete.setOnClickListener { showDeleteDialog(holder, movies[position]) }
holder.view.btnEdit.setOnClickListener { showUpdateDialog(holder, movies[position]) }
你几乎已经完成。 您只需要授予您的应用访问外部服务器(互联网)的权限。 将以下内容添加到AndroidManifest.xml
的<application>
节点上方。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/auth_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingBottom="8pt"
android:text="Initializing authorization"
style="@style/Base.TextAppearance.AppCompat.Medium"/>
<ProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"/>
<Button
android:id="@+id/auth_button"
style="@style/Widget.AppCompat.Button.Colored"
android:text="Login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
现在运行您的应用程序。 您应该能够添加,编辑和删除所有持久化到后端的内容,并且每个操作都带有对话框以确认或提供详细信息。 您刚刚制作了完整的CRUD客户端应用程序解决方案!
使用Android和Kotlin为安全的移动应用程序添加身份验证
大多数现代应用程序都需要一定级别的安全性,因此有必要知道如何快速轻松地添加身份验证。 为此,您将使用OktaAppAuth包装器库。
为什么选择Okta?
Okta的目标是使身份管理比您以往更加轻松,安全和可扩展。 Okta是一项云服务,允许开发人员创建,编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。 我们的API使您能够:
注册一个永久免费的开发人员帐户 ,完成后,创建一个新的本机应用程序,并记下客户端ID和重定向URI。
添加OAuth 2.0授权服务器
首先,您需要将服务器转换为OAuth资源(由Okta管理)。 将以下内容添加到build.gradle
。
compile 'com.okta.spring:okta-spring-boot-starter:0.5.0'
compile 'org.springframework.boot:spring-boot-starter-security'
compile 'org.springframework.security.oauth:spring-security-oauth2:2.2.0.RELEASE'
compile 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.0.1.RELEASE'
现在,将以下导入添加到您的Application.kt
。
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer
还可以使用@EnableResourceServer
注释该类。 然后创建一个名为src/main/resource/application.properties
:
okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default
okta.oauth2.clientId={clientId}
登录时,可以在Okta管理网站的“应用程序”选项卡中获取客户端ID值。
现在,当您重新启动Spring Boot并尝试匿名访问您的API时,您将收到授权错误。
C:\Users\Karl>curl localhost:8080/movies
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
添加Android AppAuth插件
要使我们的应用使用Okta,您需要使用Android的AppAuth插件。 首先创建一个新的活动来容纳登录过程。
转到新建 -> 活动 -> 空活动,并将其LoginActivity
。 在创建的activity_login.xml
放置以下内容:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/auth_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingBottom="8pt"
android:text="Initializing authorization"
style="@style/Base.TextAppearance.AppCompat.Medium"/>
<ProgressBar
android:id="@+id/progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"/>
<Button
android:id="@+id/auth_button"
style="@style/Widget.AppCompat.Button.Colored"
android:text="Login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
现在,在AndroidManifest.xml
内部交换登录活动和主要活动的名称,以便在启动时启动登录。
您已经添加了一个进度条,该进度条将一直保持到与Okta的授权连接建立。 完成后,将其隐藏,然后显示一个登录按钮。 为此,您需要更改LoginActivity.kt
。 但首先,将以下内容添加到build.gradle
。
implementation 'com.okta.android:appauth-android:0.1.0'
这将拉入Android的Okta AppAuth库。 您还需要将support lib版本更改为25.3.1 ,以使其与此库兼容,因此请将对其他版本(例如27.1.1
)的任何引用更改为该版本。 另外,将minSdkVersion更改为16
并将目标SDK更改为25
。 最后,将以下内容添加到defaultConfig
:
android.defaultConfig.manifestPlaceholders = [
// match the protocol of your "Login redirect URI"
"appAuthRedirectScheme": "com.oktapreview.dev-628819"
]
现在一切都应该建立良好。
接下来,将LoginActivity.kt
的内容更改为以下内容:
class LoginActivity : AppCompatActivity() {
private var mOktaAuth: OktaAppAuth? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mOktaAuth = OktaAppAuth.getInstance(this)
setContentView(R.layout.activity_login)
mOktaAuth!!.init(
this,
object : OktaAppAuth.OktaAuthListener {
override fun onSuccess() {
auth_button.visibility = View.VISIBLE
auth_message.visibility = View.GONE
progress_bar.visibility = View.GONE
}
override fun onTokenFailure(ex: AuthorizationException) {
auth_message.text = ex.toString()
progress_bar.visibility = View.GONE
auth_button.visibility = View.GONE
}
}
)
val button = findViewById(R.id.auth_button) as Button
button.setOnClickListener { v ->
val completionIntent = Intent(v.context, MainActivity::class.java)
val cancelIntent = Intent(v.context, LoginActivity::class.java)
cancelIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
mOktaAuth!!.login(
v.context,
PendingIntent.getActivity(v.context, 0, completionIntent, 0),
PendingIntent.getActivity(v.context, 0, cancelIntent, 0)
)
}
}
}
这将尝试初始化Okta身份验证,并在完成后隐藏进度栏和消息,并显示登录按钮。 现在,如果您尝试运行它,将会看到一个错误。 我们需要在项目中添加Okta应用程序详细信息
配置您的Okta应用
创建app/src/main/res/raw/okta_app_auth_config.json
并将以下内容放入其中:
{
"client_id": "{clientId}",
"redirect_uri": "{redirectUriValue}",
"scopes": ["openid", "profile", "offline_access"],
"issuer_uri": "https://{yourOktaDomain}/oauth2/default"
}
您应该从Okta管理控制台中的应用程序获取clientId
和redirectUriValues
。
现在,当您重新启动应用程序时,应该会看到一个登录按钮。
按下它将带您到预建的Okta登录屏幕。 如果您使用Okta帐户上的凭据登录,则将进入我们的主要活动,但该列表将为空-并会出现401错误的Toast弹出窗口(未经授权的访问)。
将授权添加到改造中
您需要从AppAuth库中获取访问令牌,并在进行API调用时将其传递给Retrofit。 在MainActivity.kt
内部添加以下功能:
fun readAuthState(): AuthState {
val authPrefs = getSharedPreferences("OktaAppAuthState", Context.MODE_PRIVATE)
val stateJson = authPrefs.getString("state", "")
return if (!stateJson!!.isEmpty()) {
try {
AuthState.jsonDeserialize(stateJson)
} catch (exp: org.json.JSONException) {
Log.e("ERROR",exp.message)
AuthState()
}
} else {
AuthState()
}
}
它使用共享的首选项来提取Okta存储的授权数据。
现在,更改适配器的标头,以使其接受访问令牌作为字符串。
class MovieAdapter(val context: Context, val token: String?)
然后,当您在MainActivity
实例化它时,请从auth状态对象传递令牌。
adapter = MovieAdapter(this.baseContext, readAuthState().accessToken)
现在,您需要在MovieApiClient.kt
更改电影的get调用,以包括授权标头。
@GET("movies") fun getMovies(@Header("Authorization") token:String): Observable
返回适配器,将refreshMovies()
更改为使用此新标头-带有新令牌和Bearer前缀:
fun refreshMovies() {
client.getMovies("Bearer $token")
}
您需要对其他方法(添加,删除,更新)进行相同的更改,以使这些功能可以与经过身份验证的后端一起使用。
应该是这样。 重新部署该应用程序,您应该像以前一样进入列表–这次已通过验证!
恭喜你! 您刚刚制作了一个完整的客户端-服务器解决方案,它具有健壮且兼容的REST后端,以及使用最新技术(全部采用中央行业标准身份验证)在前端使用Android应用程序。 您可以在GitHub上找到本教程中创建的示例。
了解有关Android,Java和安全身份验证的更多信息
我们还编写了其他一些很棒的Spring Boot和Android教程,如果您有兴趣的话,请查看它们。
最后,如果您想了解更多关于Kotlin一个伟大的地方看看,一旦你与它的工作对一个位是Kotlin成语页面 。
如有任何疑问,请随时在下面发表评论,或在我们的Okta开发者论坛上向我们提问。 如果您想查看更多类似的教程,请在Twitter @oktadev上关注我们!
“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。
最初于2018年9月11日在Okta开发者博客上发布了``使用Kotlin在Android中构建基本的CRUD应用程序'' 。
翻译自: https://www.javacodegeeks.com/2018/09/kotlin-build-basic-android-crud-app.html