Android Fragment 最佳实践

所谓知之者不如好之者,好之者不如乐之者。要想持之以恒,最佳的状态便是乐在其中。本文图文并茂,带你从Android Fragment最佳实践的“全世界”路过。
P.S. 这应该是你能找到的关于Fragment小型实践的最全分析了

先贴上效果图:

效果图

这是一个关于水果的项目(麻雀虽小,五脏俱全嘛(;゚∀゚)=3ハァハァ):
整个界面分为两个部分,左半部分是一个列表,用于显示水果的名称。
右半部分则用来显示水果的名称及简介。

既然是面向对象编程,
我们首先就需要建立一个水果的实体类Fruit来模拟这些水果。
不过,由于我们需要对这些水果进行管理,所以这里我们考虑建立一个名为AllFruit的外部类来对这些水果进行管理。
当然,并不是说非得建立这种内外部类的关系,只是这样的设计更加自然,符合逻辑。
代码如下:

public class AllFruit {

//定义内部类Fruit
    public static class Fruit{//Fruit类开始
//Fruit类的成员变量
        public Integer id;
        public String name;
        public String description;

        //Fruit类的构造器
        public Fruit(Integer id, String name, String description){
            this.id = id;
            this.name = name;
            this.description = description;
        }

//toString方法可以理解为水果对自己的介绍。这里我们重写之后,
//当直接输出水果对象时会调用此方法,此处输出水果的name属性
        @Override
        public String toString() {
            return name;
        }
    }//Fruit类结束

    //用list集合来记录所有的水果对象
    public  static List<Fruit> FRUIT_LIST_ITEMS = new ArrayList<>();
    //使用map集合来记录所有的水果对象
    public static Map<Integer,Fruit> FRUIT_MAP_ITEMS = new HashMap<>();

    static {
        //通过静态初始化块来事先定义List和Map集合中的内容
        //添加Fruit对象的方法是:用封装好的addItem()方法来添加:
        addItem(new Fruit(1,"Apple","苹果" +
                "是蔷薇科苹果亚科苹果属植物,其树为落叶乔木。" +
                "苹果的果实富含矿物质和维生素,是人们最常食用的水果之一。"));
        addItem(new Fruit(2,"Pear","梨是一种水果的名称,蔷薇科梨属植物," +
                "多年生落叶乔木果树,叶子卵形,花多白色," +
                "一般梨的颜色为外皮呈现出金黄色或暖黄色," +
                "里面果肉则为通亮白色,鲜嫩多汁,口味甘甜," +
                "核味微酸,是很好的水果。很多分布在华北、东北、西北及长江流域各省。"));
        addItem(new Fruit(3,"Banana","香蕉,又称甘蕉、芎蕉、芽蕉,弓蕉," +
                "为芭蕉科芭蕉属小果野蕉的人工栽培杂交种,为多年生草本植物。" +
                "果实长有棱;果皮黄色,果肉白色,味道香甜。" +
                "主要生长在热带、亚热带地区。原产于亚洲东南部热带、亚热带地区。"));
        addItem(new Fruit(4,"grape","葡萄为葡萄科葡萄属木质藤本植物,小枝圆柱形,有纵棱纹," +
                "无毛或被稀疏柔毛,叶卵圆形,圆锥花序密集或疏散,基部分枝发达," +
                "果实球形或椭圆形,花期4-5月,果期8-9月。"+
                "葡萄是世界最古老的果树树种之一,葡萄的植物化石发现于第三纪地层中," +
                "说明当时已遍布于欧、亚及格陵兰。[1]  葡萄原产亚洲西部,世界各地均有栽培,[2]  " +
                "世界各地的葡萄约95%集中分布在北半球。"));
    }
//封装在集合中添加水果对象的方法
    private static void addItem(Fruit fruit){
        FRUIT_LIST_ITEMS.add(fruit);
        FRUIT_MAP_ITEMS.put(fruit.id,fruit);
    }

}

用图示来说明的话,其逻辑是酱婶儿的:

图一:关于水果类的说明

一目了然,外部类AllFruit管理着内部类Fruit。

  • AllFruit将Fruit类的信息以两种方式保存:List和Map;
  • 如果别人来找AllFruit要Fruit的信息,则AllFruit就会根据具体情况来选择不同的方式以交付信息
注:关于静态初始化块的说明:
  1. Java 中可以通过初始化块进行数据赋值;
  2. 静态初始化块只在类(本例中是AllFruit类)加载时执行,且只会执行一次,同时静态初始化块只能给静态变量赋值,不能初始化普通的成员变量。
第二步:

既然已经定义好了实体类,那我们就要开始考虑项目的两个部分了。

项目的结构

这里我们先考虑右半部分的实现:
这部分实际是一个包含两个文本框的Fragment:
先贴出其布局文件:

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

    <!--定义一个TextView来显示水果名称-->
    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="40sp"
        android:layout_marginBottom="5dp"
        android:padding="20dp"
        android:textColor="@color/colorAccent"/>

    <!--再定义一个TextView来显示水果的简介-->
    <TextView
        android:id="@+id/fruit_desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:padding="30dp"/>

就定义了两个文本框而已。十分简单,不再赘述。
重点是我们要怎样将布局文件引入Fragment呢?
Android已经为我们做好了安排:

public class FruitTitleAndDescFragment extends android.app.Fragment {
    public static final String ITEM_ID = "item_id";
    //在全局变量中保存 这个Fragment将要显示的Fruit对象
    AllFruit.Fruit fruit;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments().containsKey(ITEM_ID)){
            fruit = AllFruit.FRUIT_MAP_ITEMS
.get(getArguments().getInt(ITEM_ID));
        }

    }

    //重写onCreateView()方法,此方法返回的view将作为Fragment显示的组件
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater,
 @Nullable ViewGroup container, 
@Nullable Bundle savedInstanceState) {
        View parentView = inflater.inflate(
R.layout.fruit_name_and_desc,
container,
false);
//fruit为之前定义的全局变量
        if (fruit!= null){
            //让id为fruit_name的文本框显示水果的name
            //注意setText()方法前面有几个括号
            ((TextView)parentView
.findViewById(R.id.fruit_name))
.setText(fruit.name);
            //让id为fruit_desc的文本框显示水果的desc(描述信息)
            ((TextView)parentView
.findViewById(R.id.fruit_desc))
.setText(fruit.description);
        }
        return parentView;
    }
}

这里我们定义了一个FruitTitleAndDescFragment类,并让其继承自Fragment类。
并且重写了onCreate()方法和onCreateView()方法。
其中,onCreateView()方法就是用来将Fragment的实现类与其布局进行绑定的。

具体方法为:
  • 调用onCreateView方法传入的inflater对象的inflate方法,
    传入三个参数,其中后两个一般都是container和false,而本例子中的R.layout.fruit_name_and_desc正是我们想要加载的布局。这样我们就得到了根布局,这里我们将其命名为parentView。
  • 再通过parentView的findViewById()方法就可以得到各个 子view
  • 然后我们让得到的两个文本框分别显示Fruit对象的name和description属性
  • 从这里我们也可以看出来 onCreateView()方法和单纯的setContentView()方法的区别:
    调用后者只是为了加载布局,而调用前者则是要兼顾加载布局和对布局中组件的行为进行设置。比如此处我们便设置了文本框显示的内容。
    有一定英语基础的童鞋应该很好理解:所谓onCreateView所表达的,正是view被Create之后,该做什么!
  • 最后,别忘了要将parentView返回。

而onCreate()方法则是在创建Fragment对象时被调用。
因为与后面的逻辑联系较紧密,因此其中的代码我们稍后再作讨论。

第三步:

现在我们再来考虑左半部分的实现。
左半部分是一个列表。可能有童鞋的第一反应就是用ListView来实现。但其实Android中已经为我们准备好了一个ListFragment类。让我们的活动继承自这个类,就能轻松实现列表的形式:

//直接继承自ListFragment
public class FruitListFragment extends ListFragment{

//接口的实例    
private Callback myCallback;

//在FruitListFragment中定义一个接口
    public interface Callback{ 
        void onItemClicked (Integer id);
    }//接口结束


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //为该ListFragment设置Adapter
       setListAdapter(new ArrayAdapter<AllFruit.Fruit>(
getActivity(),
               android.R.layout.simple_list_item_activated_1,
               android.R.id.text1,
AllFruit.FRUIT_LIST_ITEMS));
    }//onCreate()方法结束

//    当Fragment被添加、显示到Activity中时,回调此方法
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
//        如果Activity没有实现Callback接口,则抛出异常
        if (!(context instanceof Callback)){
            throw new IllegalStateException(
                    "FruitListFragment 所在的Activity必须实现Callback接口"
            );
        }
        //把该Activity当做Callback对象
        myCallback = (Callback) context;
    }//onAttach()结束

//    当该Fragment从它所属的Activity中被删除时回调此方法
    @Override
    public void onDetach() {
        super.onDetach();
//        给myCallback赋空值
        myCallback = null;
    }//onDetach()结束

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
//        触发myCallback的onItemclicked()方法
        myCallback.onItemClicked(AllFruit.FRUIT_LIST_ITEMS.get(position).id);
    }//onListItemClick()结束
}

这里我们定义了一个FruitListFragment类。
并重写了四个方法。
下面一一讲解:

  • onCreate()方法很好理解:
  1. 其规定了本ListFragment被创建时应该执行那些操作。
  2. 这里我们只做了一件事: 就是调用setListAdapter()方法,为要显示的列表指定了Adapter,以绑定数据源。
  3. 为setListAdapter()方法传入的是ArrayAdapter的实例:
ArrayAdapter的构造器(image from Google

可以看到,ArrayAdapter共有5个重载的构造器,我们选用的正是第五个。

  1. 第一个参数是context。这个是自然;
    第二个参数是resource,其实也就是列表中每一项的Layout;
    第三个参数是textViewResourceId,指的是列表中每一项所包含的TextView的样子(我们的水果名正是用TextView显示出来的)。
    第二个和第三个参数,在本例中都是引用的Android内置的主题。
  2. 最后一个参数最重要,因为其提供了数据的来源。这里我们指定为AllFruit.FRUIT_LIST_ITEMS。也就是先前在AllFruit中定义的list集合。
  • onAttach()方法在Fragment被添加、显示到Activity中时回调。
  1. “Attach”表示附着。正体现了Fragment与Activity的联系。
  2. 可以看到,回调时传入了一个context对象,由于Activity是Context的子类,所以可以此处传入的context对象所指代的,正是该Fragment所依附的Activity。
  3. 并且这里我们将这个传入的活动赋给了一个接口对象,关于这样做的目的,以及在FruitListFragment内部定义一个名为Callback接口的意义,稍后再做阐释。现在先记住,myCallback是一个全局变量,也就是说它的作用域为整个类。
  • onDetach()方法与onAttach()方法正相反
  1. 所以,正因为在onAttach()方法中将当前与Fragment产生联系的Activity赋给了myCallback。在onDetach()方法中,当Fragment与Activity不再发生联系时,就为myCallback赋空值以示关系的脱离。
  • onListItemClick()方法顾名思义,应当在FruitListFragment中的list的子项被点击时被回调。
...
  @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
//        触发myCallback的onItemclicked()方法
        myCallback.onItemClicked(
AllFruit.FRUIT_LIST_ITEMS.get(position).id);
    }//onListItemClick()结束

可以看到,我们在其方法体中调用了 Callback 接口中定义的方法:

onItemClicked(Integer id)

旨在设置当list的子项被点击时应该执行的逻辑。

那么问题就来了。

既然onItemClicked(Integer id)方法被定义在接口中,那么它自然是一个抽象方法。
那么这个抽象方法应该由谁来实现呢?当然是由实现了接口的那个类来实现了。
那么又由谁来实现该接口呢?

答案是:MainActivity

(跑得太远,都快忘了还有MainActivity存在了)
那么这就关系到MainActivity存在的初衷了。
我们需要MainActivity来做什么呢?
先看看其布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="?dividerHorizontal"
    android:orientation="horizontal">

    <fragment
        android:id="@+id/fruit_list"    android:name="com.example.feverdg.fragmenttest.FruitListFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <FrameLayout
        android:id="@+id/fruit_desc_container"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3" />
</LinearLayout>

可以看到,这其中包括我们事先定义好的FruitListFragment片段。用于显示列表项。
而下面这个却不是fragment标签,而是一个FrameLayout的标签
等等,那我们之前

public class FruitTitleAndDescFragment extends android.app.Fragment { ... }

定义FruitTitleAndDescFragment片段定义了半天是为了什么?
当然是为了显示了。可是为什么不用fragment标签来引用呢?
这是因为我们并不是想要直接将这个类显示在MainActivity中。
而是要通过点击不同的FruitListFragment的子项来动态地生成FruitTitleAndDescFragment 片段。
也正是为了完成这个点击事件的回调方法,我们才定义了Callback接口,并让MainActivity必须实现这个接口:

public class MainActivity extends AppCompatActivity implements FruitListFragment.Callback {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
//实现Callback接口中定义的抽象方法
    @Override
    public void onItemClicked(Integer id) {
//创建一个Bundle,以向因为点击事件而将要生成的Fragment中传入参数
        Bundle arg = new Bundle();
        arg.putInt(FruitTitleAndDescFragment.ITEM_ID, id);
//        创建FruitTitleAndDescFragment对象
        FruitTitleAndDescFragment fragment = new FruitTitleAndDescFragment();
//        向fragment中传入参数
        fragment.setArguments(arg);
//        用fragment替换fruit_desc_container中正在显示的Fragment
        android.app.FragmentManager fragmentManager = getFragmentManager();
        android.app.FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.fruit_desc_container,fragment);
        transaction.addToBackStack(null);
        transaction.commit();
    }
}

可以看到:

  • 我们首先创建一个Bundle,以向因为点击事件而将要生成的Fragment中传入参数;
  • 然后调用Bundle类的putInt()方法向该Bundle对象中传入数据。
putInt()方法 (image from Google)

可以看到putInt()方法接收的其实就是键值对: String类型的键,int类型的值。
这里传入的

arg.putInt(FruitTitleAndDescFragment.ITEM_ID, id);

FruitTitleAndDescFragment.ITEM_ID看上去好像很复杂,其实各位童鞋如果记性好的话,会发现这不过个定义在FruitTitleAndDescFragment类中的字符串常量:

public class FruitTitleAndDescFragment extends android.app.Fragment {
    public static final String ITEM_ID = "item_id";
...

键值对的键有了,那这个int类型的id值又是打哪儿来呢?
还记得我们在FruitListFragment中重写的onListItemClick()方法吗?

public void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
//        触发myCallback的onItemclicked()方法
        myCallback.onItemClicked(AllFruit.FRUIT_LIST_ITEMS
.get(position).id);
    }

我们首先用Fruit类对象的list集合 AllFruit.FRUIT_LIST_ITEM 调用get()方法;
为get()方法传入重写onListItemClick()方法时传入的position参数;
这就得到了具体是哪一个Fruit子项被点击,然后再得到该子项的id。

...
//        创建FruitTitleAndDescFragment对象
        FruitTitleAndDescFragment fragment = new FruitTitleAndDescFragment();
//        向fragment中传入参数
        fragment.setArguments(arg);
//        用fragment替换fruit_desc_container中正在显示的Fragment
        android.app.FragmentManager fragmentManager = getFragmentManager();
        android.app.FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.fruit_desc_container,fragment);
//将片段加入返回栈,也就是当按下back键后,会显示先前被replace掉的片段
        transaction.addToBackStack(null);
        transaction.commit();

准备就绪之后,我们便创建一个新的FruitTitleAndDescFragment的对象。并调用setArguments(arg)方法为其指定了参数,也就是那个带着id信息的Bundle对象。
现在知道前面FruitTitleAndDescFragment类中onCreate()方法的作用了吧:

 @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments().containsKey(ITEM_ID)){
            fruit = AllFruit.FRUIT_MAP_ITEMS
.get(getArguments().getInt(ITEM_ID));
        }
    }

只要是有新的FruitTitleAndDescFragment对象被Create,onCreate()方法就会执行。
然后我们就能够通过Bundle对象的getArguments()和getInt()方法接收到被点击的水果对象的id信息。
最后再以该id为键在之前定义的水果对象的map集合中查到对应的值。

另外,替换Fragment的操作其实很简单:
无非就是先得到FragmentManager,然后再由它创建FragmentTransaction。FragmentTransaction对象再调用replace方法替换和commit方法执行就可以了。
唯一需要注意的就是replace()方法的参数。R.id.fruit_desc_container指的就是FrameLayout的id。也很简单,不再赘述。

当然,最重要的也最精彩的部分还是Callback接口的创建。

其巧妙之处就在于:

  • MainActivity实现Callback接口之后,它就可以被视作是一个Callback对象了。
  • 所以MainActivity自然能调用接口中的onItemCliked()方法。
  • 而这个所谓的MainActivity正是我们念叨了半天的会与被创建出来的FruitTitleAndDescFragment对象不断产生和失去关联的那个Activity。
  • 所以我们在onAttach()和onDetach()方法中都可以引用到这个Activity
//    当Fragment被添加、显示到Activity中时,回调此方法
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
//        如果Activity没实现Callback接口,则抛出异常
        if (!(context instanceof Callback)){
            throw new IllegalStateException(
                    "FruitListFragment 所在的Activity必须实现Callback接口"
            );
        }
        //把该Activity当做Callback对象
        myCallback = (Callback) context;
    }//onAttach()结束

//    当该Fragment从它所属的Activity中被删除时回调此方法
    @Override
    public void onDetach() {
        super.onDetach();
//        给myCallback赋空值
        myCallback = null;
    }//onDetach()结束
  • 而也正是因为我们在onAttach()方法中通过
 //把该Activity当做Callback对象
        myCallback = (Callback) context;

获得了对MainActivity的引用,我们才得以在onListItemClick()方法中通过myCallback来调用接口中定义的onItemCliked()方法。

  • 如此,我们才得以在每次点击FruitListFragment列表中的子项时都能在MainActivity的FrameLayout布局部分生成一个新的FruitTitleAndDescFragment的实例。
  • 看上去也就像是,点击左边列表中的水果名,就能在右边列表显示其相关的name和description信息。

--- The End

事无巨细地分析,不知不觉就有点长篇累牍了。但愿能为各位同道中人填点坑吧!
水平有限,难免纰漏文中如有不当之处欢迎批评指正。
诸君共勉:)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,832评论 25 707
  • Fragment Fragment,碎片,是 Android 3.0 开始引入的,其目的在于同时兼容手机和平板的开...
    AshengTan阅读 1,726评论 0 4
  • 我是第一次看到初生儿,觉得可真丑啊!小脸蛋上全是褶子和脂肪粒,被产道挤压的脑袋像胡萝卜。但我想这是我的宝贝女...
    一霏姐姐学写作阅读 305评论 0 1
  • https://openreview.net/group?id=ICLR.cc/2017/conference O...
    FinlayLiu阅读 947评论 0 1
  • 在写文章之前,先问问各位做销售的朋友,当客户拒绝你的时候,你是如何对待这客户的拒绝的呢? 一般来说,大部分的销售新...
    SI玲阅读 360评论 0 1